├── .gitignore ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Ruby.sublime-settings ├── CSS.sublime-settings ├── Abacus.sublime-settings ├── README.md ├── Main.sublime-menu └── Abacus.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+]"], "command": "abacus" 4 | } 5 | ] 6 | 7 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["super+ctrl+alt+]"], "command": "abacus" 4 | } 5 | ] 6 | 7 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+]"], "command": "abacus" 4 | } 5 | ] 6 | 7 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Abacus: align selection", 4 | "command": "abacus" 5 | } 6 | ] -------------------------------------------------------------------------------- /Ruby.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "com.khiltd.abacus.separators": 3 | [ 4 | { 5 | "token": "=>", 6 | "gravity": "right", 7 | "preserve_indentation": true 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /CSS.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "com.khiltd.abacus.separators": 3 | [ 4 | { 5 | "token": ":", 6 | "gravity": "left", 7 | "preserve_indentation": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Abacus.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "com.khiltd.abacus.debug": true, 3 | "com.khiltd.abacus.separators": 4 | [ 5 | { 6 | "token": ":", 7 | "gravity": "left", 8 | "preserve_indentation": true 9 | }, 10 | { 11 | "token": "=", 12 | "gravity": "right", 13 | "preserve_indentation": true 14 | }, 15 | { 16 | "token": "+=", 17 | "gravity": "right", 18 | "preserve_indentation": true 19 | }, 20 | { 21 | "token": "-=", 22 | "gravity": "right", 23 | "preserve_indentation": true 24 | }, 25 | { 26 | "token": "*=", 27 | "gravity": "right", 28 | "preserve_indentation": true 29 | }, 30 | { 31 | "token": "/=", 32 | "gravity": "right", 33 | "preserve_indentation": true 34 | }, 35 | { 36 | "token": "?=", 37 | "gravity": "right", 38 | "preserve_indentation": true 39 | }, 40 | { 41 | "token": "||=", 42 | "gravity": "right", 43 | "preserve_indentation": true 44 | }, 45 | { 46 | "token": "%=", 47 | "gravity": "right", 48 | "preserve_indentation": true 49 | }, 50 | { 51 | "token": "==", 52 | "gravity": "right", 53 | "preserve_indentation": true 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Abacus Alignment Plugin for Sublime Text 2 2 | ================ 3 | 4 | ![This work?](http://dl.dropbox.com/u/5514249/Abacus.gif) 5 | 6 | I'm pretty anal about aligning things in my code, but the alignment plugins I tried were more-or-less one-trick-ponies, and I didn't like any of their tricks, so I made my own. 7 | 8 | My one anal pony trick involves allowing you to slide the operator like an abacus bead, toward either the left or the right hand side, by giving each possible token a `gravity` property like so: 9 | 10 | ``` json 11 | { 12 | "com.khiltd.abacus.separators": 13 | [ 14 | { 15 | "token": ":", 16 | "gravity": "left", 17 | "preserve_indentation": true 18 | }, 19 | { 20 | "token": "=", 21 | "gravity": "right", 22 | "preserve_indentation": true 23 | } 24 | ] 25 | } 26 | ``` 27 | 28 | Abacus focuses on aligning assignments in as language-agnostic a manner as possible and strives to address most of the open issues in that other, more popular plugin (it won't even jack up your Backbone routes!). It is, however, an *alignment* tool and *not* a full-blown beautifier. It works best when there's one assignment per line; if you like shoving dozens of CSS or JSON declarations on a single line then you are an enemy of readability and this plugin will make every effort to hinder and harm your creature on Earth as far as it is able. 29 | 30 | `preserve_indentation` is a tip that you might be working in a language where whitespace is significant, thereby suggesting that Abacus should make no effort to normalize indentation across lines. It's not foolproof, especially if you set your tab width really, really low, but it tries harder than Cory Doctorow ever has. OK, you're right... It would be impossible for anyone to try harder than that. 31 | 32 | Usage 33 | ============ 34 | 35 | Make a selection, then `command + option + control + ]`. 36 | 37 | Think the plugin's crazy? Add the following to your config: 38 | 39 | ``` 40 | "com.khiltd.abacus.debug": true 41 | ``` 42 | 43 | and Abacus will dump its thoughts out to Sublime Text's console like so: 44 | 45 | ``` 46 | margin:0; 47 | ^ 48 | padding:0; 49 | ^ 50 | border-style:none; 51 | ^ 52 | ``` 53 | 54 | Caveats 55 | ============ 56 | 57 | I've used nothing but Macs since 1984 and do absolutely **no** testing in Windows or Ububian's window manager of the minute. If something's broken in some OS I don't own, you'll need to have a suggestion as to how it can be fixed as I'm unlikely to have any idea what you're talking about. 58 | 59 | I don't care if you like real tabs or Windows line endings and don't bother with handling them. Seriously, what year is this? 60 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "selection", 4 | "children": 5 | [ 6 | { "id": "abacus" }, 7 | { 8 | "command": "abacus", 9 | "caption": "Abacus Align" 10 | } 11 | ] 12 | }, 13 | { 14 | "caption": "Preferences", 15 | "mnemonic": "n", 16 | "id": "preferences", 17 | "children": 18 | [ 19 | { 20 | "caption": "Package Settings", 21 | "mnemonic": "P", 22 | "id": "package-settings", 23 | "children": 24 | [ 25 | { 26 | "caption": "Abacus", 27 | "children": 28 | [ 29 | { 30 | "command": "open_file", 31 | "args": { "file": "${packages}/Abacus/Abacus.sublime-settings" }, 32 | "caption": "Settings - Default" 33 | }, 34 | { 35 | "command": "open_file", 36 | "args": { "file": "${packages}/User/Abacus.sublime-settings" }, 37 | "caption": "Settings - User" 38 | }, 39 | { 40 | "command": "open_file_settings", 41 | "caption": "Settings - Syntax Specific - User" 42 | }, 43 | { 44 | "command": "open_file", 45 | "args": { 46 | "file": "${packages}/Abacus/Default (Windows).sublime-keymap", 47 | "platform": "Windows" 48 | }, 49 | "caption": "Key Bindings – Default" 50 | }, 51 | { 52 | "command": "open_file", 53 | "args": { 54 | "file": "${packages}/Abacus/Default (OSX).sublime-keymap", 55 | "platform": "OSX" 56 | }, 57 | "caption": "Key Bindings – Default" 58 | }, 59 | { 60 | "command": "open_file", 61 | "args": { 62 | "file": "${packages}/Abacus/Default (Linux).sublime-keymap", 63 | "platform": "Linux" 64 | }, 65 | "caption": "Key Bindings – Default" 66 | }, 67 | { 68 | "command": "open_file", 69 | "args": { 70 | "file": "${packages}/User/Default (Windows).sublime-keymap", 71 | "platform": "Windows" 72 | }, 73 | "caption": "Key Bindings – User" 74 | }, 75 | { 76 | "command": "open_file", 77 | "args": { 78 | "file": "${packages}/User/Default (OSX).sublime-keymap", 79 | "platform": "OSX" 80 | }, 81 | "caption": "Key Bindings – User" 82 | }, 83 | { 84 | "command": "open_file", 85 | "args": { 86 | "file": "${packages}/User/Default (Linux).sublime-keymap", 87 | "platform": "Linux" 88 | }, 89 | "caption": "Key Bindings – User" 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | ] -------------------------------------------------------------------------------- /Abacus.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin, re, sys 2 | from string import Template 3 | 4 | class AbacusCommand(sublime_plugin.TextCommand): 5 | """ 6 | Main entry point. Find candidates for alignment, 7 | calculate appropriate column widths, and then 8 | perform a series of replacements. 9 | """ 10 | def run(self, edit): 11 | candidates = [] 12 | separators = sublime.load_settings("Abacus.sublime-settings").get("com.khiltd.abacus.separators") 13 | indentor = Template("$indentation$left_col") 14 | lg_aligner = Template("$left_col$separator") 15 | rg_aligner = Template("$left_col$gutter$separator_padding$separator") 16 | 17 | #Run through the separators accumulating alignment candidates 18 | #starting with the longest ones i.e. '==' before '='. 19 | longest_first = self.sort_separators(separators) 20 | 21 | #Favor those that lean right so assignments with slice notation in them 22 | #get handled sanely 23 | for separator in [righty for righty in longest_first if righty["gravity"] == "right"]: 24 | self.find_candidates_for_separator(separator, candidates) 25 | 26 | for separator in [lefty for lefty in longest_first if lefty["gravity"] == "left"]: 27 | self.find_candidates_for_separator(separator, candidates) 28 | 29 | #After accumulation is done, figure out what the minimum required 30 | #indentation and column width is going to have to be to make every 31 | #candidate happy. 32 | max_indent, max_left_col_width = self.calc_left_col_width(candidates) 33 | 34 | #Perform actual alignments based on gravitational affinity of separators 35 | for candidate in candidates: 36 | indent = 0 37 | if not candidate["preserve_indent"]: 38 | indent = max_indent 39 | else: 40 | indent = candidate["adjusted_indent"] 41 | 42 | sep_width = len(candidate["separator"]) 43 | right_col = candidate["right_col"].strip() 44 | left_col = indentor.substitute( indentation = " " * indent, 45 | left_col = candidate["left_col"] ) 46 | #Marry the separator to the proper column 47 | if candidate["gravity"] == "left": 48 | #Separator sits flush left 49 | left_col = lg_aligner.substitute(left_col = left_col, 50 | separator = candidate["separator"] ) 51 | elif candidate["gravity"] == "right": 52 | gutter_width = max_left_col_width + max_indent - len(left_col) - len(candidate["separator"]) 53 | #Push the separator ONE separator's width over the tab boundary 54 | left_col = rg_aligner.substitute( left_col = left_col, 55 | gutter = " " * gutter_width, 56 | separator_padding = " " * sep_width, 57 | separator = candidate["separator"] ) 58 | #Most sane people will want a space between the operator and the value. 59 | right_col = " %s" % right_col 60 | #Snap the left side together 61 | left_col = left_col.ljust(max_indent + max_left_col_width) 62 | candidate["replacement"] = "%s%s\n" % (left_col, right_col) 63 | 64 | #Replace each line in its entirety 65 | full_line = self.region_from_line_number(candidate["line"]) 66 | #sys.stdout.write(candidate["replacement"]) 67 | self.view.replace(edit, full_line, candidate["replacement"]) 68 | 69 | #Scroll and muck with the selection 70 | if candidates: 71 | self.view.sel().clear() 72 | for region in [self.region_from_line_number(changed["line"]) for changed in candidates]: 73 | start_of_right_col = region.begin() + max_indent + max_left_col_width 74 | insertion_point = sublime.Region(start_of_right_col, start_of_right_col) 75 | self.view.sel().add(insertion_point) 76 | #self.view.show_at_center(insertion_point) 77 | else: 78 | sublime.status_message('Abacus - no alignment token found on selected line(s)') 79 | 80 | def sort_separators(self, separators): 81 | return sorted(separators, key=lambda sep: -len(sep["token"])) 82 | 83 | def find_candidates_for_separator(self, separator, candidates): 84 | """ 85 | Given a particular separator, loop through every 86 | line in the current selection looking for it and 87 | add unique matches to a list. 88 | """ 89 | debug = sublime.load_settings("Abacus.sublime-settings").get("com.khiltd.abacus.debug") 90 | token = separator["token"] 91 | selection = self.view.sel() 92 | new_candidates = [] 93 | for region in selection: 94 | for line in self.view.lines(region): 95 | line_no = self.view.rowcol(line.begin())[0] 96 | 97 | #Never match a line more than once 98 | if len([match for match in candidates if match["line"] == line_no]): 99 | continue 100 | 101 | #Collapse any string literals that might 102 | #also contain our separator token so that 103 | #we can reliably find the location of the 104 | #real McCoy. 105 | line_content = self.view.substr(line) 106 | collapsed = line_content 107 | 108 | for match in re.finditer(r"(\"[^\"]*(?' 113 | #And remember that quoted strings were collapsed 114 | #up above! 115 | token_pos = None 116 | safe_token = re.escape(token) 117 | token_matcher = re.compile(r"(?= self.tab_width / 2: 149 | initial_indent = self.snap_to_next_boundary(initial_indent, self.tab_width) 150 | else: 151 | initial_indent -= initial_indent % self.tab_width 152 | candidate = { "line": line_no, 153 | "original": line_content, 154 | "separator": sep, 155 | "gravity": separator["gravity"], 156 | "adjusted_indent": initial_indent, 157 | "preserve_indent": separator.get("preserve_indentation", False), 158 | "left_col": left_col.lstrip(), 159 | "right_col": right_col.rstrip() } 160 | new_candidates.append(candidate) 161 | #Poke more stuff in the accumulator 162 | candidates.extend(new_candidates) 163 | 164 | def calc_left_col_width(self, candidates): 165 | """ 166 | Given a list of lines we've already matched against 167 | one or more separators, loop through them all to 168 | normalize their indentation and determine the minimum 169 | possible column width that will accomodate them all 170 | when aligned to a tab stop boundary. 171 | """ 172 | max_width = 0 173 | max_indent = 0 174 | max_sep_width = 0 175 | 176 | for candidate in candidates: 177 | max_indent = max([candidate["adjusted_indent"], max_indent]) 178 | max_sep_width = max([len(candidate["separator"]), max_sep_width]) 179 | max_width = max([len(candidate["left_col"].rstrip()), max_width]) 180 | 181 | max_width += max_sep_width 182 | 183 | #Bump up to the next multiple of tab_width 184 | max_width = self.snap_to_next_boundary(max_width, self.tab_width) 185 | 186 | return max_indent, max_width 187 | 188 | @property 189 | def tab_width(self): 190 | """ 191 | Exceptionally inefficient 192 | """ 193 | return int(self.view.settings().get('tab_size', 4)) 194 | 195 | def detab(self, input): 196 | """ 197 | Goodbye tabs! 198 | """ 199 | return input.expandtabs(self.tab_width) 200 | 201 | def region_from_line_number(self, line_number): 202 | """ 203 | Given a zero-based line number, return a region 204 | encompassing it (including the newline). 205 | """ 206 | return self.view.full_line(self.view.text_point(line_number, 0)) 207 | 208 | def snap_to_next_boundary(self, value, interval): 209 | """ 210 | Alignment voodoo 211 | """ 212 | return value + (interval - value % interval) 213 | --------------------------------------------------------------------------------