Beautiful Ruby in TextMate

Wednesday, 10 May 2006

If you’ve been writing Ruby code using TextMate (like me), then you might have wished for an automatic code formatting capability. I didn’t find it built into TextMate, but I did find that it was very easy to add.


TextMate is Allan Odgaard’s popular and powerful text editor for Mac OS X. It has great support for projects involving multiple files and has some very powerful hooks allowing customization. Using language or application-specific “bundles”, TextMate users can add macros, shortcuts, and even run external programs to parse, modify, and replace all or part of a file being edited.

Although TextMate currently has no built-in Ruby code beautifier, Paul Lutus has written a Ruby code beautifier in Ruby. To add it to TextMate, open TextMate’s Bundle Editor window using the Window -> Show Bundle Editor menu command. You’ll then see a window like the one below.

In the pane on the left-hand side, scroll down to the Ruby item and expand it. From the + menu at the bottom left, select the New Command menu item. You can name your command anything you like; I called mine “Beautify”. Click on the text field to the right of Activation: and press the key combination that you want to use to activate your command (I’m using Command-B). Then set the Scope Selector: field to “source.ruby” to ensure that your new command will only be run when you are editing Ruby source code. Finally, paste my slightly-modified version of Paul’s beautifier script into the command window (it’s below). When you’re finished, the Bundle Editor window should look like this:

Now any time you are editing a Ruby file, you can beautify it by pressing Command-B (or your chosen activation key). If something goes wrong or you don’t like the results, just use TextMate’s undo command (Command-Z) to go back to the original version.

The beautification code is below. I’ve made some trivial changes to Paul’s version to make it read from STDIN and write to STDOUT; since his version was released under the GPL, I believe that the GPL license terms still apply, but I waive all rights to my changes. With thanks to Paul and Allan, Happy Rubying!

#!/usr/bin/env ruby

# Ruby beautifier, version 1.3, 04/03/2006
# Copyright (c) 2006, P. Lutus
# TextMate modifications by T. Burks
# Released under the GPL

$tabSize = 2
$tabStr = " " 

# indent regexp tests

$indentExp = [
   /^module\b/,
   /(=\s*|^)if\b/,
   /(=\s*|^)until\b/,
   /(=\s*|^)for\b/,
   /(=\s*|^)unless\b/,
   /(=\s*|^)while\b/,
   /(=\s*|^)begin\b/,
   /(=\s*|^)case\b/,
   /\bthen\b/,
   /^class\b/,
   /^rescue\b/,
   /^def\b/,
   /\bdo\b/,
   /^else\b/,
   /^elsif\b/,
   /^ensure\b/,
   /\bwhen\b/,
   /\{[^\}]*$/,
   /\[[^\]]*$/
]

# outdent regexp tests

$outdentExp = [
   /^rescue\b/,
   /^ensure\b/,
   /^elsif\b/,
   /^end\b/,
   /^else\b/,
   /\bwhen\b/,
   /^[^\{]*\}/,
   /^[^\[]*\]/
]

def makeTab(tab)
   return (tab < 0)?"":$tabStr * $tabSize * tab
end

def addLine(line,tab)
   line.strip!
   line = makeTab(tab)+line if line.length > 0
   return line + "\n" 
end

def beautifyRuby
   commentBlock = false
   multiLineArray = Array.new
   multiLineStr = "" 
   tab = 0
   source = STDIN.read
   dest = "" 
   source.split("\n").each do |line|
      # combine continuing lines
      if(!(line =~ /^\s*#/) && line =~ /[^\\]\\\s*$/)
         multiLineArray.push line
         multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
         next
      end

      # add final line
      if(multiLineStr.length > 0)
         multiLineArray.push line
         multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
      end

      tline = ((multiLineStr.length > 0)?multiLineStr:line).strip
      if(tline =~ /^=begin/)
         commentBlock = true
      end
      if(commentBlock)
         # add the line unchanged
         dest += line + "\n" 
      else
         commentLine = (tline =~ /^#/)
         if(!commentLine)
            # throw out sequences that will
            # only sow confusion
            tline.gsub!(/\/.*?\//,"")
            tline.gsub!(/%r\{.*?\}/,"")
            tline.gsub!(/%r(.).*?\1/,"")
            tline.gsub!(/\\\"/,"'")
            tline.gsub!(/".*?"/,"\"\"")
            tline.gsub!(/'.*?'/,"''")
            tline.gsub!(/#\{.*?\}/,"")
            $outdentExp.each do |re|
               if(tline =~ re)
                  tab -= 1
                  break
               end
            end
         end
         if (multiLineArray.length > 0)
            multiLineArray.each do |ml|
               dest += addLine(ml,tab)
            end
            multiLineArray.clear
            multiLineStr = "" 
         else
            dest += addLine(line,tab)
         end
         if(!commentLine)
            $indentExp.each do |re|
               if(tline =~ re && !(tline =~ /\s+end\s*$/))
                  tab += 1
                  break
               end
            end
         end
      end
      if(tline =~ /^=end/)
         commentBlock = false
      end
   end
   STDOUT.write(dest)
   # uncomment this to complain about mismatched blocks
   #if(tab != 0)
   #  STDERR.puts "Indentation error: #{tab}" 
   #end 
end

beautifyRuby 
Comments (3) post a reply
  1. cornelius Wednesday, 08 Aug 2007, 12:18 AM PDT

    Here is an updated version (2.1), based on the latest release of the original script:

    #!/usr/bin/env ruby
    
    # Ruby beautifier, version 2.1, 09/11/2006
    # Copyright (c) 2006, P. Lutus
    # Released under the GPL
    
    # Modificated to work as a Textmate command
    
    $tabSize = 2
    $tabStr = " " 
    
    # indent regexp tests
    
    $indentExp = [
       /^module\b/,
       /^if\b/,
       /(=\s*|^)until\b/,
       /(=\s*|^)for\b/,
       /^unless\b/,
       /(=\s*|^)while\b/,
       /(=\s*|^)begin\b/,
       /(^| )case\b/,
       /\bthen\b/,
       /^class\b/,
       /^rescue\b/,
       /^def\b/,
       /\bdo\b/,
       /^else\b/,
       /^elsif\b/,
       /^ensure\b/,
       /\bwhen\b/,
       /\{[^\}]*$/,
       /\[[^\]]*$/
    ]
    
    # outdent regexp tests
    
    $outdentExp = [
       /^rescue\b/,
       /^ensure\b/,
       /^elsif\b/,
       /^end\b/,
       /^else\b/,
       /\bwhen\b/,
       /^[^\{]*\}/,
       /^[^\[]*\]/
    ]
    
    def makeTab(tab)
       return (tab < 0) ? "" : $tabStr * $tabSize * tab
    end
    
    def addLine(line,tab)
       line.strip!
       line = makeTab(tab)+line if line.length > 0
       return line + "\n" 
    end
    
    def beautifyRuby
       commentBlock = false
       programEnd = false
       multiLineArray = Array.new
       multiLineStr = "" 
       tab = 0
       source = STDIN.read
       dest = "" 
       source.split("\n").each do |line|
          if(!programEnd)
             # detect program end mark
             if(line =~ /^__END__$/)
                programEnd = true
             else
                # combine continuing lines
                if(!(line =~ /^\s*#/) && line =~ /[^\\]\\\s*$/)
                   multiLineArray.push line
                   multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
                   next
                end
    
                # add final line
                if(multiLineStr.length > 0)
                   multiLineArray.push line
                   multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
                end
    
                tline = ((multiLineStr.length > 0)?multiLineStr:line).strip
                if(tline =~ /^=begin/)
                   commentBlock = true
                end
             end
          end
          if(commentBlock || programEnd)
             # add the line unchanged
             dest += line + "\n" 
          else
             commentLine = (tline =~ /^#/)
             if(!commentLine)
                # throw out sequences that will
                # only sow confusion
                while tline.gsub!(/'.*?'/,"")
                end
                while tline.gsub!(/".*?"/,"")
                end
                while tline.gsub!(/\`.*?\`/,"")
                end
                while tline.gsub!(/\{[^\{]*?\}/,"")
                end
                while tline.gsub!(/\([^\(]*?\)/,"")
                end
                while tline.gsub!(/\/.*?\//,"")
                end
                while tline.gsub!(/%r(.).*?\1/,"")
                end
                tline.gsub!(/\\\"/,"'")
                $outdentExp.each do |re|
                   if(tline =~ re)
                      tab -= 1
                      break
                   end
                end
             end
             if (multiLineArray.length > 0)
                multiLineArray.each do |ml|
                   dest += addLine(ml,tab)
                end
                multiLineArray.clear
                multiLineStr = "" 
             else
                dest += addLine(line,tab)
             end
             if(!commentLine)
                $indentExp.each do |re|
                   if(tline =~ re && !(tline =~ /\s+end\s*$/))
                      tab += 1
                      break
                   end
                end
             end
          end
          if(tline =~ /^=end/)
             commentBlock = false
          end
       end
       STDOUT.write(dest)
       # uncomment this to complain about mismatched blocks
       # if(tab != 0)
       #   STDERR.puts "#{path}: Indentation error: #{tab}" 
       # end
    end
    
    beautifyRuby
  2. Wes Hays Monday, 10 Sep 2007, 11:24 AM PDT

    I tried this many times I and keep getting the same error.

    env: ruby: No such file or directory

    I have “ruby=/opt/local/bin/ruby” in my environment and if I run ”/usr/bin/env ruby” shows it is responsive.

    I tried added a path tag to ~/.MacOSX/environment.plist for a path to ”/opt/local/bin/ruby” with no luck.

    I also tried changing ”#!/usr/bin/env ruby” in the script to ”#!/opt/local/bin/ruby” but that did not work either.

    I am running Ruby version 1.8.6 and TextMate version 1.5.6 (1405).

    I tried this on several macs with the same results.

  3. cjm Monday, 10 Sep 2007, 03:22 PM PDT

    Just trying this script as well. I have found that the first one works and the second one gives the error you have been describing. Haven’t isolated the cause yet but you could try using the first script and then looking at the difference between them.

    Suspect the second script has been modified but not tested.