├── .gitignore ├── LICENSE.md ├── README.md ├── coffeelint.json ├── keymaps └── atom-alignment.cson ├── lib ├── aligner.coffee └── atom-alignment.coffee ├── menus └── atom-alignment.cson ├── package.json ├── spec └── atom-alignment-spec.coffee └── styles └── atom-alignment.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-alignment package 2 | 3 | [Atom package](https://atom.io/packages/atom-alignment) 4 | 5 | > Inspired by sublime text plugin ([sublime_alignment](https://github.com/wbond/sublime_alignment)) 6 | 7 | ## Usage 8 | 9 | A simple key-binding for aligning multi-line, multi-cursor and multiple selections in Atom. 10 | 11 | Use `ctrl+cmd+a` on Mac or `ctrl+alt+a` to align multiple matches. If you want to align the first match only, call `Atom Alignment:Align` from the command palette. The following examples all use the mentioned key binding to call `Atom Alignment:AlignMultiple`. 12 | 13 | ```javascript 14 | var a = b; 15 | var ab = c; 16 | var abcd = d; 17 | var ddddd =d; 18 | ``` 19 | 20 | ```javascript 21 | var a = b; 22 | var ab = c; 23 | var abcd = d; 24 | var ddddd = d; 25 | ``` 26 | 27 | With more than one selection 28 | 29 | ```javascript 30 | var a = b; /* selection 1 */ 31 | var ab = c; /* selection 1 */ 32 | var notMePlease='NOOOO'; 33 | var abcd = d; /* selection 2 */ 34 | var ddddd =d; /* selection 2 */ 35 | ``` 36 | 37 | ```javascript 38 | var a = b; 39 | var ab = c; 40 | var notMePlease='NOOOO'; 41 | var abcd = d; 42 | var ddddd = d; 43 | ``` 44 | 45 | On a single line started with `ctrl+cmd+a` 46 | 47 | ```javascript 48 | var a = b var cde = d 49 | ``` 50 | 51 | ```javascript 52 | var a = b 53 | var cde = d 54 | ``` 55 | 56 | When working with multiple cursors, the different lines are aligned at the best matching cursor position. In the following example the | shows the cursor position. 57 | 58 | ```javascript 59 | var a =b var c|= d 60 | var e c|= f var g = h 61 | var i c=j var k |= l 62 | ``` 63 | 64 | ```javascript 65 | var a =b var c = d 66 | var e c = f var g = h 67 | var i c=j var k = l 68 | ``` 69 | 70 | You can even align multiple matches 71 | 72 | ```javascript 73 | lets = see = what := happens 74 | a = a = b = c : d := e 75 | ``` 76 | 77 | ```javascript 78 | lets = see = what := happens 79 | a = a = b = c : d := e 80 | ``` 81 | 82 | ## License 83 | 84 | MIT © [Andre Lerche](https://github.com/papermoon1978) 85 | 86 | MIT © [Simon Paitrault](http://www.freyskeyd.fr) 87 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation" : 3 | { 4 | "level" : "error", 5 | "value" : 4 6 | }, 7 | "max_line_length" : 8 | { 9 | "level" : "error", 10 | "value" : 512 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /keymaps/atom-alignment.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 11 | '.platform-darwin atom-text-editor': 12 | 'ctrl-cmd-a': 'atom-alignment:alignMultiple' 13 | '.platform-win32 atom-text-editor, .platform-linux atom-text-editor': 14 | 'ctrl-alt-a': 'atom-alignment:alignMultiple' 15 | -------------------------------------------------------------------------------- /lib/aligner.coffee: -------------------------------------------------------------------------------- 1 | {Range} = require 'atom' 2 | 3 | _ = require 'lodash' 4 | 5 | module.exports = 6 | class Aligner 7 | # Public 8 | constructor: (@editor, @spaceChars, @matcher, @addSpacePostfix) -> 9 | @rows = [] 10 | @alignments = [] 11 | 12 | # Private 13 | __getRows: => 14 | rowNums = [] 15 | allCursors = [] 16 | cursors = _.filter @editor.getCursors(), (cursor) -> 17 | allCursors.push(cursor) 18 | row = cursor.getBufferRow() 19 | if cursor.visible && !_.contains(rowNums, row) 20 | rowNums.push(row) 21 | return true 22 | 23 | if (cursors.length > 1) 24 | @mode = "cursor" 25 | for cursor in cursors 26 | row = cursor.getBufferRow() 27 | t = @editor.lineTextForBufferRow(row) 28 | l = @__computeLength(t.substring(0,cursor.getBufferColumn())) 29 | o = 30 | text : t 31 | length : t.length 32 | row : row 33 | column : l 34 | cursor : cursor 35 | virtualColumn: cursor.getBufferColumn() 36 | @rows.push (o) 37 | 38 | else 39 | ranges = @editor.getSelectedBufferRanges() 40 | for range in ranges 41 | rowNums = rowNums.concat( 42 | _.filter range.getRows(), (rangeRow) -> 43 | return !rowNums.includes(rangeRow) 44 | ) 45 | rowNums.pop() if range.end.column == 0 46 | 47 | for row in rowNums 48 | o = 49 | text : @editor.lineTextForBufferRow(row) 50 | length : @editor.lineTextForBufferRow(row).length 51 | row : row 52 | @rows.push (o) 53 | 54 | @mode = "align" 55 | 56 | if @mode != "cursor" 57 | @rows.forEach (o) -> 58 | t = o.text.replace(/\s/g, '') 59 | if t.length > 0 60 | firstCharIdx = o.text.indexOf(t.charAt(0)) 61 | o.text = o.text.substr(0,firstCharIdx) + o.text.substring(firstCharIdx).replace(/\ {2,}/g, ' ') 62 | 63 | __getAllIndexes: (string, val, indexes) -> 64 | found = [] 65 | i = 0 66 | loop 67 | i = string.indexOf(val, i) 68 | if i != -1 && !_.some(indexes, {index:i}) 69 | found.push({found:val,index:i}) 70 | 71 | break if i == -1 72 | i++ 73 | return found 74 | 75 | #generate the sequence of alignment characters computed from the first matching line 76 | __generateAlignmentList: () => 77 | if @mode == "cursor" 78 | _.forEach @rows, (o) => 79 | part = o.text.substring(o.virtualColumn) 80 | _.forEach @spaceChars, (char) -> 81 | idx = part.indexOf(char) 82 | if idx == 0 && o.text.charAt(o.virtualColumn) != " " 83 | o.addSpacePrefix = true 84 | o.spaceCharLength = char.length 85 | return false 86 | return 87 | else 88 | _.forEach @rows, (o) => 89 | _.forEach @matcher, (possibleMatcher) => 90 | @alignments = @alignments.concat (@__getAllIndexes o.text, possibleMatcher, @alignments) 91 | 92 | if @alignments.length > 0 93 | return false # exit if we got all alignments characters in the row 94 | else 95 | return true # continue 96 | @alignments = @alignments.sort (a, b) -> a.index - b.index 97 | @alignments = _.pluck @alignments, "found" 98 | return 99 | 100 | __computeLength: (s) => 101 | diff = tabs = idx = 0 102 | tabLength = @editor.getTabLength() 103 | for char in s 104 | if char == "\t" 105 | diff += tabLength - (idx % tabLength) 106 | idx += tabLength - (idx % tabLength) 107 | tabs++ 108 | else 109 | idx++ 110 | 111 | return s.length+diff-tabs 112 | 113 | __computeRows: () => 114 | max = 0 115 | if @mode == "align" || @mode == "break" 116 | matched = null 117 | idx = -1 118 | possibleMatcher = @alignments.shift() 119 | addSpacePrefix = @spaceChars.indexOf(possibleMatcher) > -1 120 | @rows.forEach (o) => 121 | o.splited = null 122 | if !o.done 123 | line = o.text 124 | if (line.indexOf(possibleMatcher, o.nextPos) != -1) 125 | matched = possibleMatcher 126 | idx = line.indexOf(matched, o.nextPos) 127 | len = matched.length 128 | if @mode == "break" 129 | idx += len-1 130 | c = "" 131 | blankPos = -1 132 | quotationMark = doubleQuotationMark = 0 133 | backslash = charFound = false 134 | loop 135 | break if c == undefined 136 | c = line[++idx] 137 | if c == "'" and !backslash then quotationMark++ 138 | if c == '"' and !backslash then doubleQuotationMark++ 139 | backslash = if c == "\\" and !backslash then true else false 140 | charFound = if c != " " and !charFound then true else charFound 141 | if c == " " and quotationMark % 2 == 0 and doubleQuotationMark % 2 == 0 and charFound 142 | blankPos = idx 143 | break 144 | 145 | idx = blankPos 146 | 147 | next = if @mode == "break" then 1 else len 148 | 149 | if idx isnt -1 150 | splitString = [line.substring(0,idx), line.substring(idx+next)] 151 | o.splited = splitString 152 | l = @__computeLength(splitString[0]) 153 | if max <= l 154 | max = l 155 | max++ if l > 0 && addSpacePrefix && splitString[0].charAt(splitString[0].length-1) != " " 156 | 157 | found = false 158 | _.forEach @alignments, (nextPossibleMatcher) -> 159 | if (line.indexOf(nextPossibleMatcher, idx+len) != -1) 160 | found = true 161 | return false 162 | 163 | o.stop = !found 164 | 165 | return 166 | 167 | if (max >= 0) 168 | max++ if max > 0 169 | 170 | @rows.forEach (o) => 171 | if !o.done and o.splited and matched 172 | splitString = o.splited 173 | diff = max - @__computeLength(splitString[0]) 174 | if diff > 0 175 | splitString[0] = splitString[0] + Array(diff).join(' ') 176 | 177 | splitString[1] = " "+splitString[1].trim() if @addSpacePostfix && addSpacePrefix 178 | 179 | if @mode == "break" 180 | _.forEach splitString, (s, i) -> 181 | splitString[i] = s.trim() 182 | 183 | o.text = splitString.join("\n") 184 | else 185 | o.text = splitString.join(matched) 186 | o.done = o.stop 187 | o.nextPos = splitString[0].length+matched.length 188 | return 189 | return @alignments.length > 0 190 | else #cursor 191 | @rows.forEach (o) -> 192 | if max <= o.column 193 | max = o.column 194 | part = o.text.substring(0,o.virtualColumn) 195 | max++ if part.length > 0 && o.addSpacePrefix && part.charAt(part.length-1) != " " 196 | return 197 | 198 | max++ 199 | 200 | @rows.forEach (o) => 201 | line = o.text 202 | splitString = [line.substring(0,o.virtualColumn), line.substring(o.virtualColumn)] 203 | diff = max - @__computeLength(splitString[0]) 204 | if diff > 0 205 | splitString[0] = splitString[0] + Array(diff).join(' ') 206 | 207 | o.spaceCharLength ?= 0 208 | splitString[1] = splitString[1].substring(0, o.spaceCharLength) + splitString[1].substr(o.spaceCharLength).trim() 209 | if @addSpacePostfix && o.addSpacePrefix 210 | splitString[1] = splitString[1].substring(0, o.spaceCharLength) + " " +splitString[1].substr(o.spaceCharLength) 211 | 212 | o.text = splitString.join("") 213 | return 214 | return false 215 | 216 | # Public 217 | align: (multiple) => 218 | @__getRows() 219 | @__generateAlignmentList() 220 | if @rows.length == 1 && multiple 221 | @mode = "break" 222 | 223 | if multiple || @mode == "break" 224 | loop 225 | cont = @__computeRows() 226 | break if not cont 227 | else 228 | @__computeRows() 229 | 230 | checkpoint = @editor.createCheckpoint() 231 | @rows.forEach (o) => 232 | @editor.setTextInBufferRange([[o.row, 0],[o.row, o.length]], o.text) 233 | if o.cursor 234 | o.cursor.setBufferPosition([o.row, o.virtualColumn + (o.text.length - o.length)]) 235 | @editor.groupChangesSinceCheckpoint(checkpoint) 236 | -------------------------------------------------------------------------------- /lib/atom-alignment.coffee: -------------------------------------------------------------------------------- 1 | Aligner = require './aligner' 2 | 3 | module.exports = 4 | config: 5 | alignmentSpaceChars: 6 | type: 'array' 7 | default: ['=>', ':=', '='] 8 | items: 9 | type: "string" 10 | description: "insert space in front of the character (a=1 > a =1)" 11 | order: 2 12 | alignBy: 13 | type: 'array' 14 | default: ['=>', ':=', ':', '='] 15 | items: 16 | type: "string" 17 | description: "consider the order, the left most matching value is taken to compute the alignment" 18 | order: 1 19 | addSpacePostfix: 20 | type: 'boolean' 21 | default: false 22 | description: "insert space after the matching character (a=1 > a= 1) if character is part of the 'alignment space chars'" 23 | order: 3 24 | 25 | activate: (state) -> 26 | atom.commands.add 'atom-text-editor', 27 | 'atom-alignment:align': -> 28 | editor = atom.workspace.getActivePaneItem() 29 | alignLines editor 30 | 31 | 'atom-alignment:alignMultiple': -> 32 | editor = atom.workspace.getActivePaneItem() 33 | alignLinesMultiple editor 34 | 35 | alignLines = (editor) -> 36 | spaceChars = atom.config.get 'atom-alignment.alignmentSpaceChars' 37 | matcher = atom.config.get 'atom-alignment.alignBy' 38 | addSpacePostfix = atom.config.get 'atom-alignment.addSpacePostfix' 39 | a = new Aligner(editor, spaceChars, matcher, addSpacePostfix) 40 | a.align(false) 41 | return 42 | 43 | alignLinesMultiple = (editor) -> 44 | spaceChars = atom.config.get 'atom-alignment.alignmentSpaceChars' 45 | matcher = atom.config.get 'atom-alignment.alignBy' 46 | addSpacePostfix = atom.config.get 'atom-alignment.addSpacePostfix' 47 | a = new Aligner(editor, spaceChars, matcher, addSpacePostfix) 48 | a.align(true) 49 | return 50 | -------------------------------------------------------------------------------- /menus/atom-alignment.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | '.overlayer':[{'Enable atom-alignment': 'atom-alignment:align'}] 4 | 5 | 'menu': [ 6 | { 7 | 'label': 'Packages' 8 | 'submenu': [ 9 | 'label': 'Atom Alignment' 10 | 'submenu': [ 11 | { 'label': 'Align', 'command': 'atom-alignment:align' }, 12 | { 'label': 'Align Multiple', 'command': 'atom-alignment:alignMultiple' } 13 | ] 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-alignment", 3 | "main": "./lib/atom-alignment", 4 | "version": "0.13.0", 5 | "private": true, 6 | "description": "A simple key-binding for aligning multi-line and multiple selections in Atom (Based on the sublime text plugin)", 7 | "repository": "https://github.com/Freyskeyd/atom-alignment", 8 | "license": "MIT", 9 | "activationCommands": { 10 | "atom-workspace": [ 11 | "atom-alignment:align", 12 | "atom-alignment:alignMultiple" 13 | ] 14 | }, 15 | "engines": { 16 | "atom": ">0.50.0" 17 | }, 18 | "dependencies": { 19 | "lodash": "^2.4.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/atom-alignment-spec.coffee: -------------------------------------------------------------------------------- 1 | AtomAlignment = require '../lib/atom-alignment' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "AtomAlignment", -> 9 | activationPromise = null 10 | 11 | beforeEach -> 12 | atom.workspaceView = new WorkspaceView 13 | activationPromise = atom.packages.activatePackage('atomAlignment') 14 | 15 | # describe "when the atom-alignment:align event is triggered", -> 16 | # it "attaches and then detaches the view", -> 17 | # expect(atom.workspaceView.find('.atom-alignment')).not.toExist() 18 | # 19 | # # This is an activation event, triggering it will cause the package to be 20 | # # activated. 21 | # atom.workspaceView.trigger 'atom-alignment:toggle' 22 | # 23 | # waitsForPromise -> 24 | # activationPromise 25 | # 26 | # runs -> 27 | # expect(atom.workspaceView.find('.atom-alignment')).toExist() 28 | # atom.workspaceView.trigger 'atom-alignment:toggle' 29 | # expect(atom.workspaceView.find('.atom-alignment')).not.toExist() 30 | -------------------------------------------------------------------------------- /styles/atom-alignment.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .atom-alignment { 8 | } 9 | --------------------------------------------------------------------------------