Created
November 26, 2010 11:37
-
-
Save meleyal/716594 to your computer and use it in GitHub Desktop.
Align CSS Assignments Textmate command
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# | |
# Assignment block tidier, version 0.6. | |
# | |
# Copyright Chris Poirier 2006, 2008. | |
# Licensed under the Academic Free License version 3.0. | |
# | |
# This script can be used as a command for TextMate to align all of the equal signs | |
# within a block of text, as well as all variable declarations in languages like | |
# Objective-C and Java. When using it with TextMate, set the command input to | |
# "Selected Text" or "Document", and the output to "Replace Selected Text". Map it | |
# to a key equivalent, and any time you want to tidy up a block, either select it, | |
# or put your cursor somewhere within it; then hit the key equivalent. Voila. | |
# | |
# Note that this is the third version of the script, but it hasn't been heavily | |
# tested. You might encounter a bug or two. | |
# | |
# Per the license, use of this script is ENTIRELY at your own risk. See the license | |
# for full details (they override anything I've said here). | |
# | |
# ================================================================================== | |
# | |
# This script lives at: | |
# http://courage-my-friend.org/projects/ | |
# | |
# Thanks to: | |
# Guillaume Cerquant (http://www.cerquant.com/) | |
# - for ideas and code review | |
# | |
# | |
# =================================================================================== | |
# | |
# You can enable or disable various features by setting the ALIGNMENT_FEATURES | |
# environment variable in TextMate's advanced preferences. If you provide the this | |
# variable, it should include a colon (:) separated list of the feature names to | |
# enable. All others will be disabled. If you do not provide this environment | |
# variable, suitable defaults will be used. | |
# | |
# Features: | |
# align_declarations | |
# - columnate the type and variable on an typed variable declaration block | |
# | |
# outdent_star | |
# - columnate the LHS * marker in C variable declarations and assignments | |
# | |
# align_assignments | |
# - columnate the = in a block of assignment/declaration statements | |
# | |
# debug_mode | |
# - see what the script is doing | |
# | |
feature_names = [ :align_declarations, :outdent_star, :align_assignments, :debug_mode ] | |
default_enabled = [ :align_declarations, :align_assignments ] | |
user_enabled = ENV.member?("ALIGNMENT_FEATURES") ? ENV["ALIGNMENT_FEATURES"].to_s.split(":") : nil | |
enabled = {} | |
feature_names.each do |feature_name| | |
if user_enabled then | |
enabled[feature_name] = user_enabled.member?(feature_name.to_s) | |
else | |
enabled[feature_name] = default_enabled.member?(feature_name) | |
end | |
end | |
# | |
# Load the work. | |
lines = STDIN.readlines() | |
selected_text = ENV.member?("TM_SELECTED_TEXT") | |
# | |
# We care about two kinds of lines: assignment statements and variable declarations. | |
# In languages like Ruby, variable declarations *are* assignment statements. However, | |
# in Java and Objective-C, a type name can precede the variable name, and the assignment | |
# can be skipped altogether. We'll try to handle both. We'll also try to columnate the | |
# LHS * marker in Objective-C variable declarations and assignment statements, if the | |
# appropriate flag is set in enabled, above. | |
relevant_line_patterns = [] | |
left_column_indices = [] | |
right_column_indices = [] | |
ensure_space_betweens = [] | |
if enabled[:align_declarations] then | |
relevant_line_patterns << /^(\s*((?!(if|elsif|else)(\(|\s))\w+(\[[^\]]*\])*)((\s*(?:\*))|\s+))?((\**\s*\w+(\[[^\]]*\])*)\s*(:|\;))/ | |
left_column_indices << 1 | |
right_column_indices << 8 | |
ensure_space_betweens << true | |
end | |
if enabled[:outdent_star] then | |
relevant_line_patterns << /^((\s*(?!(if|elsif|else)(\(|\s))\w+(\[[^\]]*\])*((\s*\*+\s*)|\s+))|((\s*\*+\s*)|\s+))(\w+(\[[^\]]*\])*\s*(:|\;))/ | |
left_column_indices << 1 | |
right_column_indices << 10 | |
ensure_space_betweens << false | |
end | |
if enabled[:align_assignments] then | |
relevant_line_patterns << /^([^:()]+)(:[>]?.*)/ | |
left_column_indices << 1 | |
right_column_indices << 2 | |
ensure_space_betweens << true | |
relevant_line_patterns << /^([^:()]+:[>]?)(.*)/ | |
left_column_indices << 1 | |
right_column_indices << 2 | |
ensure_space_betweens << true | |
end | |
# | |
# Let's simplify life, by extending =~ and !~ to arrays of patterns. | |
class Array | |
def =~( string ) | |
i = 1 | |
self.each { |pattern| return i if string =~ pattern; i += 1 } | |
return false | |
end | |
end | |
# | |
# If called on a selection, every assignment statement | |
# is in the block. If called on the document, we start on the | |
# current line and look up and down for the start and end of the | |
# block. | |
if selected_text then | |
block_top = 1 | |
block_bottom = lines.length | |
else | |
# | |
# We start looking on the current line. However, if the | |
# current line doesn't match the pattern, we may be just | |
# after or just before a block, and we should check. If | |
# neither, we are done. | |
start_on = ENV["TM_LINE_NUMBER"].to_i | |
block_top = lines.length + 1 | |
block_bottom = 0 | |
search_top = 1 | |
search_bottom = lines.length | |
search_failed = false | |
if lines[start_on - 1] !~ relevant_line_patterns then | |
if lines[start_on - 2] =~ relevant_line_patterns then | |
search_bottom = start_on = start_on - 1 | |
elsif lines[start_on] =~ relevant_line_patterns then | |
search_top = start_on = start_on | |
else | |
search_failed = true | |
end | |
end | |
# | |
# Now with the search boundaries set, start looking for | |
# the block top and bottom. | |
unless search_failed | |
start_on.downto(search_top) do |number| | |
if lines[number-1] =~ relevant_line_patterns then | |
block_top = number | |
else | |
break | |
end | |
end | |
start_on.upto(search_bottom) do |number| | |
if lines[number-1] =~ relevant_line_patterns then | |
block_bottom = number | |
else | |
break | |
end | |
end | |
end | |
end | |
# | |
# Now, things get a bit more complicated. We want to turn this: | |
# x = 10 | |
# String x = 10; | |
# LongClassName *xyzabc = 13; | |
# Class *xyzabc[] = { abc, def, ghi }; | |
# Class* xyzabc = &x; | |
# int i; | |
# boolean isItTrue; | |
# boolean meh = false; | |
# | |
# into this (depending on enabled, of course): | |
# x = 10 | |
# String x = 10; | |
# LongClassName *xyzabc = 13; | |
# Class *xyzabc[] = { abc, def, ghi }; | |
# Class* xyzabc = &x; | |
# int i; | |
# boolean isItTrue; | |
# boolean meh = false; | |
# | |
# Further, if there are no types on any of the lines, no space should be | |
# left for it. | |
relevant_line_patterns.length.times do |i| | |
relevant_line_pattern = relevant_line_patterns[i] | |
left_column_index = left_column_indices[i] | |
right_column_index = right_column_indices[i] | |
ensure_space_between = ensure_space_betweens[i] | |
# | |
# Iterate over the block and find the best column number for the right | |
# hand match to move to. We strip off whitespace, where appropriate, to | |
# ensure repeatable results. | |
use_pass = false | |
best_column = 0 | |
block_top.upto(block_bottom) do |number| | |
line = lines[number - 1] | |
if m = relevant_line_pattern.match(line) then | |
space_offset = 0 | |
unless m[left_column_index].nil? | |
if space_index = m[left_column_index].index(/\s+$/) then | |
space_offset = m.end(left_column_index) - space_index | |
end | |
use_pass = true | |
end | |
this_column = m.begin(right_column_index) - space_offset | |
best_column = this_column if this_column > best_column | |
end | |
end | |
# | |
# Reformat the block. | |
if use_pass && best_column > 0 then | |
block_top.upto(block_bottom) do |number| | |
line = lines[number - 1].dup | |
if m = relevant_line_pattern.match(line) then | |
prefix = m.pre_match | |
suffix = m.post_match | |
left = m[left_column_index].to_s.sub(/\s+$/, "") | |
right = m[right_column_index].to_s.sub(/^\s+/, "") | |
space = ensure_space_between ? " " : "" | |
lines[number-1] = (prefix + left).ljust(best_column) + space + right + suffix | |
$stderr.puts "#{best_column}: #{lines[number-1]}" if enabled["debug_mode"] | |
end | |
end | |
end | |
end | |
# | |
# Output the replacement text | |
lines.each do |line| | |
puts line | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment