Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: e47a983b1e
Fetching contributors…

Cannot retrieve contributors at this time

file 210 lines (180 sloc) 10.224 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
import sublime, sublime_plugin, re, sys
from string import Template

def abacus_settings():
    return sublime.load_settings("Abacus.sublime-settings")

class AbacusCommand(sublime_plugin.TextCommand):
    """
Main entry point. Find candidates for alignment,
calculate appropriate column widths, and then
perform a series of replacements.
"""
    def run(self, edit):
        candidates = []
        separators = abacus_settings().get("com.khiltd.abacus.separators")
        indentor = Template("$indentation$left_col")
        lg_aligner = Template("$left_col$separator")
        rg_aligner = Template("$left_col$gutter$separator_padding$separator")

        #Run through the separators accumulating alignment candidates
        #starting with the longest ones i.e. '==' before '='.
        longest_first = self.sort_separators(separators)

        #Favor those that lean right so assignments with slice notation in them
        #get handled sanely
        for separator in [righty for righty in longest_first if righty["gravity"] == "right"]:
            self.find_candidates_for_separator(separator, candidates)

        for separator in [lefty for lefty in longest_first if lefty["gravity"] == "left"]:
            self.find_candidates_for_separator(separator, candidates)
        
        #After accumulation is done, figure out what the minimum required
        #indentation and column width is going to have to be to make every
        #candidate happy.
        max_indent, max_left_col_width = self.calc_left_col_width(candidates)

        #Perform actual alignments based on gravitational affinity of separators
        for candidate in candidates:
            indent = 0
            if not candidate["preserve_indent"]:
                indent = max_indent
            else:
                indent = candidate["adjusted_indent"]

            sep_width = len(candidate["separator"])
            right_col = candidate["right_col"].strip()
            left_col = indentor.substitute( indentation = " " * indent,
                                                left_col = candidate["left_col"] )
            #Marry the separator to the proper column
            if candidate["gravity"] == "left":
                #Separator sits flush left
                left_col = lg_aligner.substitute(left_col = left_col,
                                                    separator = candidate["separator"] )
            elif candidate["gravity"] == "right":
                gutter_width = max_left_col_width + max_indent - len(left_col) - len(candidate["separator"])
                #Push the separator ONE separator's width over the tab boundary
                left_col = rg_aligner.substitute( left_col = left_col,
                                                        gutter = " " * gutter_width,
                                                        separator_padding = " " * sep_width,
                                                        separator = candidate["separator"] )
                #Most sane people will want a space between the operator and the value.
                right_col = " %s" % right_col
            #Snap the left side together
            left_col = left_col.ljust(max_indent + max_left_col_width)
            candidate["replacement"] = "%s%s\n" % (left_col, right_col)
            
            #Replace each line in its entirety
            full_line = self.region_from_line_number(candidate["line"])
            #sys.stdout.write(candidate["replacement"])
            self.view.replace(edit, full_line, candidate["replacement"])
            
        #Scroll and muck with the selection
        self.view.sel().clear()
        for region in [self.region_from_line_number(changed["line"]) for changed in candidates]:
            start_of_right_col = region.begin() + max_indent + max_left_col_width
            insertion_point = sublime.Region(start_of_right_col, start_of_right_col)
            self.view.sel().add(insertion_point)
            #self.view.show_at_center(insertion_point)
            
    def sort_separators(self, separators):
        return sorted(separators, key=lambda sep: -len(sep["token"]))

    def find_candidates_for_separator(self, separator, candidates):
        """
Given a particular separator, loop through every
line in the current selection looking for it and
add unique matches to a list.
"""
        debug = abacus_settings().get("com.khiltd.abacus.debug")
        token = separator["token"]
        selection = self.view.sel()
        new_candidates = []
        for region in selection:
            for line in self.view.lines(region):
                line_no = self.view.rowcol(line.begin())[0]
            
                #Never match a line more than once
                if len([match for match in candidates if match["line"] == line_no]):
                    continue
 
                #Collapse any string literals that might
                #also contain our separator token so that
                #we can reliably find the location of the
                #real McCoy.
                line_content = self.view.substr(line)
                collapsed = line_content

                for match in re.finditer(r"(\"[^\"]*(?<!\\)\"|'[^']*(?<!\\)')", line_content):
                    quoted_string = match.group(0)
                    collapsed = collapsed.replace(quoted_string, "\007" * len(quoted_string))
                    
                #Look for ':' but not '::', '=' but not '=>'
                #And remember that quoted strings were collapsed
                #up above!
                token_pos = None
                safe_token = re.escape(token)
                token_matcher = re.compile(r"(?<![^a-zA-Z0-9_ \007])%s(?![^a-zA-Z0-9_ \007])" % (safe_token))
                potential_matches = [m for m in token_matcher.finditer(collapsed)]
                
                if debug:
                    print token_matcher.pattern
                    print potential_matches
                
                if len(potential_matches):
                    #Split on the first/last occurrence of the token
                    if separator["gravity"] == "right":
                        token_pos = potential_matches[-1].start()
                    elif separator["gravity"] == "left":
                        token_pos = potential_matches[0].start()
                        
                    #Do you see what I see?
                    if debug:
                        sys.stdout.write("%s\n" % line_content)
                        sys.stdout.write(" " * token_pos)
                        sys.stdout.write("^\n")
                    
                    #Now we can slice
                    left_col = self.detab(line_content[:token_pos]).rstrip()
                    right_col = self.detab(line_content[token_pos + len(token):])
                    sep = line_content[token_pos:token_pos + len(token)]
                    initial_indent = re.match("\s+", left_col) or 0
                    
                    if initial_indent:
                        initial_indent = len(initial_indent.group(0))
                        #Align to tab boundary
                        if initial_indent % self.tab_width >= self.tab_width / 2:
                            initial_indent = self.snap_to_next_boundary(initial_indent, self.tab_width)
                        else:
                            initial_indent -= initial_indent % self.tab_width
                    candidate = { "line": line_no,
                                        "original": line_content,
                                        "separator": sep,
                                        "gravity": separator["gravity"],
                                        "adjusted_indent": initial_indent,
                                        "preserve_indent": separator.get("preserve_indentation", False),
                                        "left_col": left_col.lstrip(),
                                        "right_col": right_col.rstrip() }
                    new_candidates.append(candidate)
        #Poke more stuff in the accumulator
        candidates.extend(new_candidates)

    def calc_left_col_width(self, candidates):
        """
Given a list of lines we've already matched against
one or more separators, loop through them all to
normalize their indentation and determine the minimum
possible column width that will accomodate them all
when aligned to a tab stop boundary.
"""
        max_width = 0
        max_indent = 0
        max_sep_width = 0

        for candidate in candidates:
            max_indent = max([candidate["adjusted_indent"], max_indent])
            max_sep_width = max([len(candidate["separator"]), max_sep_width])
            max_width = max([len(candidate["left_col"].rstrip()), max_width])
        
        max_width += max_sep_width

        #Bump up to the next multiple of tab_width
        max_width = self.snap_to_next_boundary(max_width, self.tab_width)
                    
        return max_indent, max_width
    
    @property
    def tab_width(self):
        """
Exceptionally inefficient
"""
        return int(self.view.settings().get('tab_size', 4))

    def detab(self, input):
        """
Goodbye tabs!
"""
        return input.expandtabs(self.tab_width)
        
    def region_from_line_number(self, line_number):
        """
Given a zero-based line number, return a region
encompassing it (including the newline).
"""
        return self.view.full_line(self.view.text_point(line_number, 0))

    def snap_to_next_boundary(self, value, interval):
        """
Alignment voodoo
"""
        return value + (interval - value % interval)
Something went wrong with that request. Please try again.