From ba45106f55cf79d549f4716d0ba50f724fff461e Mon Sep 17 00:00:00 2001 From: James Edward Gray II Date: Sun, 29 Nov 2009 16:56:00 -0600 Subject: [PATCH] A first pass at a much smarter block toggling command. It's now able to cope with scenarios like braces inside a String literal and it properly handles some nesting. This restores the command to working order after it broke due to an Oniguruma update in the last TextMate update. --- ...230{ \342\200\246 }\342\200\231.tmCommand" | 150 ++++++++++++++++++ Macros/Toggle Single : Multi Line Block.plist | 63 -------- Support/bin/toggle_block.rb | 94 ----------- info.plist | 5 +- 4 files changed, 153 insertions(+), 159 deletions(-) create mode 100644 "Commands/Toggle \342\200\230do \342\200\246 end\342\200\231 : \342\200\230{ \342\200\246 }\342\200\231.tmCommand" delete mode 100644 Macros/Toggle Single : Multi Line Block.plist delete mode 100755 Support/bin/toggle_block.rb diff --git "a/Commands/Toggle \342\200\230do \342\200\246 end\342\200\231 : \342\200\230{ \342\200\246 }\342\200\231.tmCommand" "b/Commands/Toggle \342\200\230do \342\200\246 end\342\200\231 : \342\200\230{ \342\200\246 }\342\200\231.tmCommand" new file mode 100644 index 0000000..4f36440 --- /dev/null +++ "b/Commands/Toggle \342\200\230do \342\200\246 end\342\200\231 : \342\200\230{ \342\200\246 }\342\200\231.tmCommand" @@ -0,0 +1,150 @@ + + + + + beforeRunningCommand + nop + command + #!/usr/bin/env ruby -wKU +# encoding: UTF-8 + +require "rexml/text" +require "#{ENV["TM_SUPPORT_PATH"]}/lib/escape" + +# transform XML into normal and sanitized Ruby +ruby, safe_ruby, scope = "", "", [] +STDIN.read.scan(/<(.*?)>|([^<]+)/) do + if $1 + if $1[0] == ?/ + scope.pop + else + scope.push($1) + end + else + unescaped = REXML::Text.unnormalize($2) + ruby << unescaped + # strip strings, regexes, and comments from safe_ruby but leave byte count + if scope.any? { |s| s =~ /\A(?:string|comment)\b/ } + safe_ruby << " " * unescaped.length + else + safe_ruby << unescaped + end + end +end + +# find and mark the cursor +line_number = ENV["TM_LINE_NUMBER"].to_i +input_start_line = ENV["TM_INPUT_START_LINE"].to_i +row = line_number - input_start_line +col = ENV["TM_LINE_INDEX"].to_i +if line_number == input_start_line + col -= ENV["TM_INPUT_START_LINE_INDEX"].to_i +end +cursor = ruby[/\A(?:.*\n){#{row - 1}}.{#{col}}/].size +CURSOR = [0xFFFC].pack("U") +B = "(?:\\b|#{CURSOR})" # Note: /\w/u includes CURSOR +ruby[cursor, 0] = CURSOR +safe_ruby[cursor, 0] = CURSOR + +# find the block nearest to the cursor +block_start, block_length = nil, nil +loop do + block_start = safe_ruby.rindex( /(\{|#{B}do#{B})/, + block_start.nil? ? cursor : block_start - 1 ) + if block_start.nil? # block not found: give up and don't change the document + print e_sn(ruby).sub(CURSOR, "$0") + exit + end + block_length, nesting = 0, [] + if $1 == "{" + re, starts, stop = /\{|\}|[^{}]+/, ["{"], "}" + else + re, starts, stop = /#{B}do#{B}|#{B}end#{B}|./m, ["do"], "end" + end + safe_ruby[block_start..-1].scan(re) do |token| + block_length += token.length + token.sub!(/\A#{CURSOR}/, "") + token.sub!(/#{CURSOR}\z/, "") + case token + when *starts + nesting << token + when stop + if nesting.last.nil? + nesting << nil + break + else + nesting.pop + break if nesting.empty? + end + end + end + break if nesting.empty? and ruby[block_start, block_length].include? CURSOR +end +block = ruby[block_start, block_length] + +# toggle the block +if block[0] == ?{ + block = block[1..-2] + if block.include? "\n" + block[0, 0] = " " if block =~ /\A#{CURSOR}?[A-Za-z0-9_]/ + block << " " if block =~ /[A-Za-z0-9_]#{CURSOR}?\z/ + block = "do#{block}end" + else # expand the block + block.strip! + lines = %w[do] + if block.sub!(/\A(#{CURSOR}?(\s*)\|[^|]*\|)/, "") + lines.first << "#{' ' if $2.empty?}#{$1}" + end + indent = ruby[0...block_start][/^([ \t]*).*\Z/, 1] + tab = ( ENV["TM_SOFT_TABS"] == "YES" ? " " * ENV["TM_TAB_SIZE"].to_i : + "\t" ) + lines << "#{indent}#{tab}#{block.strip}" + lines << "#{indent}end" + block = lines.join("\n") + end +else + block = block[2..-4] + if block.include? "\n" # collapse the block + lines = block.send(block.respond_to?(:lines) ? :lines : :to_s).to_a + lines.first.send( + "#{'r' if lines.first =~ /\A\s*#{CURSOR}?\s*\|[^|]*\|/}strip!" + ) + lines[1..-1].each do |line| + line.strip! + end + lines.first << "; " unless lines.first =~ + /\A\s*#{CURSOR}?\s*(?:\|[^|]*\|)?\s*#{CURSOR}?\z/ + lines.first << " " unless lines.first =~ /\s\z/ + lines[1..-2].each do |line| + line << "; " + end + lines[-2].sub!(/; \z/, "") if lines.size > 2 and lines.last.empty? + cursor_by_end = lines.size > 2 && lines.last == CURSOR + lines[-2].sub!(/; \z/, " ") if cursor_by_end + block = "{#{lines.join}#{' ' unless cursor_by_end}}" + else + block = "{#{block}}" + end +end + +# replace document +print e_sn(ruby[0...block_start]) +print e_sn(block).sub(CURSOR, "$0") +print e_sn(ruby[(block_start + block_length)..-1]) + + input + selection + inputFormat + xml + keyEquivalent + ^{ + name + Toggle ‘do … end’ / ‘{ … }’ + output + insertAsSnippet + scope + source.ruby + uuid + 59E811FF-E722-46BE-8938-04713612FABB + + diff --git a/Macros/Toggle Single : Multi Line Block.plist b/Macros/Toggle Single : Multi Line Block.plist deleted file mode 100644 index b049053..0000000 --- a/Macros/Toggle Single : Multi Line Block.plist +++ /dev/null @@ -1,63 +0,0 @@ - - - - - commands - - - argument - - command - insertText: - - - argument - - action - findPrevious - findInProjectIgnoreCase - - findInProjectRegularExpression - - findString - \{(?m:.*?\x{FFFC}.*?)\}|do\b(?m:.*?\x{FFFC}.*?)\bend\b - ignoreCase - - regularExpression - - replaceAllScope - document - replaceString - - wrapAround - - - command - findWithOptions: - - - argument - - command - toggle_block.rb - input - selection - output - insertAsSnippet - - command - executeCommandWithOptions: - - - keyEquivalent - ^{ - name - Toggle ‘do … end’ / ‘{ … }’ - scope - source.ruby - scopeType - local - uuid - 7990EE60-C850-4779-A8C0-7FD2C853B99B - - diff --git a/Support/bin/toggle_block.rb b/Support/bin/toggle_block.rb deleted file mode 100755 index ae2a153..0000000 --- a/Support/bin/toggle_block.rb +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: utf-8 - -# FIXME: In a ruby code line like "lines.each { |line| puts "#{i += 1}. " + line }", the "switch between {} and do-end" command picks up the wrong braces. -# allan: JEG2: maybe we should just let a script do the extraction rather than the current regexp find in a macro -# allan: but with a script it’s fairly easy — we can even let the script take XML as input so that it can leverage TMs parser to find code blocks etc. - -require "escape" - -# Unicode code point 0xFFFC -CURSOR = [0xFFFC].pack("U").freeze - -def toggle_block( block_text ) - result = block_text.dup - - case result.to_a.size - when 1 # single line to multi-line transform - # find the '}' relative to the cursor - if c = result.index(CURSOR) - i = result.index("}", c) or raise "'}' not found." - else - i = result.rindex("}") or raise "'}' not found." - end - # convert - result[i, 1] = "\nend" - - # find '{' with proper nesting allowed - i = result.rindex("{") or raise "'{' not found" - while (nested = result.rindex("}")) and nested > i and i > 0 - if prev = result.rindex("{", i - 1) - i = prev - else - break - end - end - # convert - if i > 0 and result[i - 1, 1][/\w/] - result[i, 1] = " do" - i += 1 - else - result[i, 1] = "do" - end - - # drop in the newline after the parameters, if present - if params = result[(i + 2)..-1][/([ \t]*\|[^|]*\|)([ \t]*)/, 1] - result[i + 2 + params.size, 0] = "\n" - result[i + 2 + params.size + 1, $2.size] = "" unless $2.empty? - else - result[i + 2, 0] = "\n" - if result[(i + 3)..-1][/[ \t]*/] - result[(i + 3), $&.size] = "" - end - end - - # properly indent the remaining lines - indent = result[/\A[ \t]*/] - lines = result.to_a - result = [ lines.first, - lines[1..-2].map { |line| line[indent.size, 0] = "\t"; line }, - lines.last ].join - - # strip trailing line whitespace - result.sub!(/(\S)[ \t]+\n/, "\\1\n") - when 2..1.0/0.0 # multi-line to single line transform - lines = result.to_a - lines.each { |line| line.chomp! } - - # flop 'do' to '{' - d = lines.shift - i = d.index(/\bdo\b/) or raise "'do' not found." - d[i, 2] = "{" - d << "; " unless d[(i + 1)..-1] =~ /\A\s*?(?:\|[^|]*\|)?\s*\Z/ - d << " " unless d =~ /\s+\Z/ - - # flop 'end' to '}' - (e = lines.pop).sub!(/\A\s*(\S*)\s*\bend\b\s*/, "\\1 }") \ - or raise "'end' not found." - e = "; " + e unless e =~ /\A\s+/ - - # rejoin lines, adding ';'s as needed - lines.each { |line| line.strip! } - result = [d, lines.join("; "), e].join - else - raise "No lines to convert." - end - - result -rescue # if anything goes wrong... - block_text # just send back the original text -end - -if __FILE__ == $PROGRAM_NAME - print e_sn(toggle_block(STDIN.read)).gsub(CURSOR, "$0") -end diff --git a/info.plist b/info.plist index 74bf373..efcf9e6 100644 --- a/info.plist +++ b/info.plist @@ -20,6 +20,7 @@ 66708259-62C3-11D9-B8CF-000D93589AF6 66708052-62C3-11D9-B8CF-000D93589AF6 6670881E-62C3-11D9-B8CF-000D93589AF6 + 7990EE60-C850-4779-A8C0-7FD2C853B99B description Support for the <a href="http://www.ruby-lang.org/">Ruby</a> programming language. @@ -290,7 +291,7 @@ items - 7990EE60-C850-4779-A8C0-7FD2C853B99B + 59E811FF-E722-46BE-8938-04713612FABB ------------------------------------ 855FC4EF-7B1E-48EE-AD4E-5ECB8ED79D1C 4B72C5C3-6CA7-41AC-B2F9-51DEA25D469E @@ -442,12 +443,12 @@ B297E4B8-A8FF-49CE-B9C4-6D4911724D43 835FAAC6-5431-436C-998B-241F7226B99B 0275EF39-9357-408F-AF20-79E415CA9504 + 59E811FF-E722-46BE-8938-04713612FABB 97054C4D-E4A3-45B1-9C00-B82DBCB30CAD 569C9822-8C41-4907-94C7-1A8A0031B66D 6519CB08-8326-4B77-A251-54722FFBFC1F 47D203ED-EB9B-4653-A07B-A897800CEB76 C122CD92-DDBE-4869-9C7A-CC2B254C9411 - 7990EE60-C850-4779-A8C0-7FD2C853B99B 121B334B-2AA6-4E9A-A8B8-BF93B627982B 58FDEA60-10AF-4C49-AA09-29B77030DB25 33969819-62C5-4E03-B824-C2337205F364