├── .gitignore ├── .travis.yml ├── keymaps └── surround.cson ├── spec ├── spec-helper.coffee └── vim-surround-spec.coffee ├── package.json ├── lib ├── command │ ├── surround.coffee │ ├── selector.coffee │ ├── delete.coffee │ ├── base.coffee │ └── change.coffee └── vim-surround.coffee ├── LICENSE.md ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | APM_TEST_PACKAGES="vim-mode" sh' 9 | 10 | git: 11 | depth: 10 12 | -------------------------------------------------------------------------------- /keymaps/surround.cson: -------------------------------------------------------------------------------- 1 | # OOPS! Good job with your dilligent hunting, but vim-surround's keybindings 2 | # are actually completely dynamic, so it doesn't use this file.... So I'll use 3 | # this as a guest book. Send me a PR! 4 | 5 | # # OFFICIAL VIM-SURROUND GUEST BOOK :+1: 6 | 7 | # George Marchin ~ Hi Mom! 8 | -------------------------------------------------------------------------------- /spec/spec-helper.coffee: -------------------------------------------------------------------------------- 1 | getEditorElement = (callback) -> 2 | textEditor = null 3 | 4 | waitsForPromise -> 5 | atom.project.open().then (e) -> 6 | textEditor = e 7 | 8 | runs -> 9 | element = document.createElement("atom-text-editor") 10 | element.setModel(textEditor) 11 | callback(element) 12 | 13 | module.exports = { getEditorElement } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vim-surround", 3 | "main": "./lib/vim-surround", 4 | "version": "0.8.1", 5 | "description": "vim-surround for Atom!", 6 | "repository": "https://github.com/gepoch/vim-surround", 7 | "license": "MIT", 8 | "keywords": [ 9 | "pair", 10 | "parentheses", 11 | "quotes", 12 | "surround", 13 | "vim" 14 | ], 15 | "engines": { 16 | "atom": ">0.50.0" 17 | }, 18 | "dependencies": {}, 19 | "consumedServices": {} 20 | } 21 | -------------------------------------------------------------------------------- /lib/command/surround.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | Base = require './base' 4 | 5 | module.exports = class Surround extends Base 6 | constructor: (config) -> 7 | @command = config.surroundCommand 8 | @context = "atom-text-editor.vim-mode.visual-mode" 9 | super config 10 | 11 | getName: (key) -> "surround-#{key}" 12 | 13 | getRunner: (left, right) -> -> 14 | editor = atom.workspace.getActiveTextEditor() 15 | editor.transact -> 16 | editor.selections.forEach (selection) -> 17 | text = selection.getText() 18 | selection.insertText "#{left}#{text}#{right}" 19 | -------------------------------------------------------------------------------- /lib/command/selector.coffee: -------------------------------------------------------------------------------- 1 | vimModePath = atom.packages.resolvePackagePath('vim-mode') or 2 | atom.packages.resolvePackagePath('vim-mode-next') 3 | 4 | {SelectInsideQuotes, SelectInsideBrackets} = require "#{vimModePath}/lib/text-objects" 5 | 6 | module.exports = class Selector 7 | constructor: (@editor, left, right) -> 8 | @left = left.trim() 9 | @right = right.trim() 10 | 11 | inside: -> 12 | if @isBraket() 13 | new SelectInsideBrackets(@editor, @left, @right, false) 14 | else 15 | new SelectInsideQuotes(@editor, @left, false) 16 | 17 | outside: -> 18 | if @isBraket() 19 | new SelectInsideBrackets(@editor, @left, @right, true) 20 | else 21 | new SelectInsideQuotes(@editor, @left, true) 22 | 23 | isBraket: -> 24 | ['[', ']', '{', '}', '<', '>', '(', ')'].indexOf?(@left.trim()) >= 0 25 | -------------------------------------------------------------------------------- /lib/command/delete.coffee: -------------------------------------------------------------------------------- 1 | {compositedisposable} = require 'atom' 2 | 3 | Base = require './base' 4 | Selector = require './selector' 5 | 6 | module.exports = class Delete extends Base 7 | constructor: (config) -> 8 | @command = config.deleteSurroundCommand 9 | @context = "atom-text-editor.vim-mode.normal-mode" 10 | super config 11 | 12 | getName: (key) -> "delete-#{key}" 13 | 14 | getRunner: (left, right) -> -> 15 | editor = atom.workspace.getActiveTextEditor() 16 | selector = new Selector(editor, left, right) 17 | 18 | editor.transact -> 19 | cursorPos = editor.getCursorBufferPosition() 20 | 21 | selector.inside().select() 22 | editor.selections.forEach (selection) -> 23 | text = selection.getText() 24 | 25 | # restore cursore and select text with surrounding keys 26 | editor.setCursorBufferPosition(cursorPos) 27 | selector.outside().select() 28 | 29 | editor.selections.forEach (selection) -> 30 | selection.insertText text 31 | 32 | # restore cursore 33 | editor.setCursorBufferPosition(cursorPos) 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 George Marchin 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 | -------------------------------------------------------------------------------- /lib/vim-surround.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | Surround = require './command/surround' 4 | Delete = require './command/delete' 5 | Change = require './command/change' 6 | 7 | module.exports = 8 | config: 9 | pairs: 10 | type: 'array' 11 | default: ['()', '{}', '[]', '""', "''", "``"] 12 | items: 13 | type: 'string' 14 | changeSurroundCommand: 15 | type: 'string' 16 | default: 'c s' 17 | deleteSurroundCommand: 18 | type: 'string' 19 | default: 'd s' 20 | surroundCommand: 21 | type: 'string' 22 | default: 's' 23 | deleteCommand: 24 | type: 'string' 25 | default: 'd s' 26 | 27 | activate: (state) -> 28 | @commandClasses = [ 29 | Surround, Delete, Change 30 | ] 31 | 32 | @configLoop = atom.config.observe 'vim-surround', (config) => 33 | @disposables.dispose() if @disposables? 34 | @disposables = new CompositeDisposable 35 | 36 | @commands = [] 37 | 38 | for cls in @commandClasses 39 | command = new cls config 40 | @commands.push command 41 | @disposables.add command.disposables 42 | 43 | consumeVimMode: (vimMode) -> @vimMode = vimMode 44 | 45 | deactivate: () -> @disposables.dispose() 46 | -------------------------------------------------------------------------------- /lib/command/base.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | module.exports = class Base 4 | 5 | constructor: (config) -> 6 | @disposables = new CompositeDisposable 7 | 8 | @curPairs = [] 9 | @registerPairs config.pairs 10 | 11 | registerPairs: (pairs) -> 12 | pairs = (x for x in pairs when x.length > 0 and x.length %2 == 0) 13 | 14 | for pair in pairs 15 | if pair not in @curPairs 16 | @registerPair pair 17 | @curPairs.push(pair) 18 | 19 | registerPair: (pair) -> 20 | [left, right] = @splitPair(pair) 21 | 22 | if left != right 23 | @createPairBindings left, "#{left} ", " #{right}" 24 | @createPairBindings right, left, right 25 | 26 | createPairBindings: (key, left, right) -> 27 | name = "vim-surround:#{@getName key}" 28 | 29 | # First, we add a command to the system to actually perform the surround. 30 | # We attach the disposable to our object's list. 31 | @disposables.add atom.commands.add @context, name, @getRunner left, right 32 | 33 | # Next, we build up keybindings for our command. First, we build a 34 | # space-delineated version of our key that's passed in. This breaks up 35 | # double keys like `{%` into the seperate keystroke form: `{ %` 36 | keys = "" 37 | for i in [0..key.length-1] 38 | if i == 0 39 | keys = key[i] 40 | else 41 | keys = "#{keys} #{key[i]}" 42 | 43 | # Making a one-command keymap data structure here. Basically: 44 | # "atom-text-editor.vim-mode.visual-mode": 45 | # "s (": 46 | # "vim-surround:surround-(" 47 | 48 | keymapArg = {} 49 | fullCommand = "#{@command} #{keys}" 50 | keymapArg[fullCommand] = name 51 | 52 | contextArg = {} 53 | contextArg[@context] = keymapArg 54 | 55 | # Capture the disposable heretom test! 56 | @disposables.add atom.keymaps.add name, contextArg 57 | 58 | splitPair: (pair) -> 59 | return [pair[..(pair.length/2)-1], pair[pair.length/2..]] 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.8.1 2 | * Backticks enabled by default. 3 | 4 | ## 0.8.0 5 | * Tentative support for 6 | [vim-mode-next](https://atom.io/packages/vim-mode-next). See 7 | [#28](https://github.com/gepoch/vim-surround/issues/28). 8 | 9 | ## 0.7.4 10 | * Bugfixes. 11 | 12 | ## 0.7.3 13 | * Fixed problems cause by vim-mode changing "command-mode" to "normal-mode". 14 | 15 | ## 0.7.1 16 | * Propagate README changes. 17 | 18 | ## 0.7.0 19 | * Change surround and delete surround implemented. Thanks to @shemerey :D ! 20 | 21 | ## 0.6.1 22 | * Fixed keymap -> keymaps change. [vim-surround #19](https://github.com/gepoch/vim-surround/issues/19) 23 | 24 | ## 0.6.0 25 | * Upgraded to new config schema. General restructuring. 26 | 27 | ## 0.5.1 28 | * Handle undefined values in config. 29 | 30 | ## 0.5.0 31 | * Configurable surround key to support muscle memory in response to [vim-surround #12](https://github.com/gepoch/vim-surround/issues/12) 32 | 33 | ## 0.4.3 34 | * Fixed bug [vim-surround #13](https://github.com/gepoch/vim-surround/issues/13) 35 | 36 | 37 | ## 0.4.2 38 | * Fixed bug [vim-surround #11](https://github.com/gepoch/vim-surround/issues/11) 39 | * Readme updates. 40 | 41 | ## 0.4.1 42 | * Renamed the dynamic keybindings to match the package name. 43 | * Added some tests. 44 | * Poked the README. 45 | 46 | ## 0.4.0 47 | * Multiple cursor support. 48 | 49 | ## 0.3.0 50 | * Updated for Atom API v1.0.0 changes. 51 | 52 | ## 0.2.3 53 | * Fixed bug: [vim-surround #5](https://github.com/gepoch/vim-surround/issues/5) 54 | 55 | ## 0.2.0 - Activation and Keybindings Improved. 56 | * Keybindings are now dynamically generated from the configured pairs. No more 57 | user keybindings changes necessary! 58 | * Activation is now on atom boot since keybindings are now dynamic and 59 | impossible to predict for the purposes of activation events. 60 | * Fixed bug where surround action did not exit visual mode. 61 | 62 | ## 0.1.0 - First Release 63 | * Every feature added 64 | * Every bug fixed 65 | -------------------------------------------------------------------------------- /spec/vim-surround-spec.coffee: -------------------------------------------------------------------------------- 1 | helpers = require './spec-helper' 2 | 3 | describe "Vim Surround activation", -> 4 | [editor, pairs, editorElement, vimSurround, configPairs, chars, names] = [] 5 | 6 | beforeEach -> 7 | pairs = ['()', '{}', '[]', '""', "''"] 8 | atom.config.set('vim-surround.pairs', pairs) 9 | 10 | vimSurround = atom.packages.loadPackage('vim-surround') 11 | vimSurround.activate() 12 | 13 | configPairs = atom.config.get('vim-surround.pairs') 14 | 15 | helpers.getEditorElement (element) -> 16 | editorElement = element 17 | editor = editorElement.getModel() 18 | 19 | editorClassList = editorElement.classList 20 | 21 | editorClassList.add('editor') 22 | editorClassList.add('vim-mode') 23 | editorClassList.add('visual-mode') 24 | 25 | 26 | describe "When the vim-surround module loads", -> 27 | beforeEach -> 28 | chars = [] 29 | pairs.forEach (pair) -> 30 | for i in [0..pair.length-1] 31 | char = pair[i] 32 | chars.push char unless char in chars 33 | 34 | commands = atom.commands.findCommands target: editorElement 35 | 36 | names = [] 37 | commands.forEach (command) -> 38 | names.push(command.name) 39 | 40 | it "Creates a surround command for each configured pair character", -> 41 | chars.forEach (char) -> 42 | expect(names).toContain("vim-surround:surround-#{char}") 43 | 44 | describe "and the list of pairs changes", -> 45 | beforeEach -> 46 | pairs = ['()', '{}', '[]', '""', "-+"] 47 | atom.config.set('vim-surround.pairs', pairs) 48 | commands = atom.commands.findCommands target: editorElement 49 | names = (command.name for command in commands) 50 | chars = [] 51 | pairs.forEach (pair) -> 52 | for i in [0..pair.length-1] 53 | char = pair[i] 54 | chars.push char unless char in chars 55 | 56 | it "should add any new pairs.", -> 57 | chars.forEach (char) -> 58 | expect(names).toContain("vim-surround:surround-#{char}") 59 | 60 | it "should remove any old pairs.", -> 61 | expect(names).not.toContain("vim-surround:surround-'") 62 | 63 | describe "and then deactivates", -> 64 | 65 | beforeEach -> 66 | vimSurround.deactivate() 67 | commands = atom.commands.findCommands target: editorElement 68 | names = (command.name for command in commands) 69 | 70 | it "should clear out all commands from the registry", -> 71 | chars.forEach (char) -> 72 | expect(names).not.toContain("vim-surround:surround-#{char}") 73 | -------------------------------------------------------------------------------- /lib/command/change.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | Base = require './base' 4 | Selector = require './selector' 5 | 6 | module.exports = class Change 7 | constructor: (config) -> 8 | @command = config.changeSurroundCommand 9 | @context = "atom-text-editor.vim-mode.normal-mode" 10 | @disposables = new CompositeDisposable 11 | @curPairs = [] 12 | @curPairsWithTarget = [] 13 | @registerPairs config.pairs 14 | 15 | getName: (key, targetKey) -> "change-#{key}-to-#{targetKey}" 16 | 17 | registerPairs: (pairs) -> 18 | pairs = (x for x in pairs when x.length > 0 and x.length %2 == 0) 19 | 20 | for pair in pairs 21 | for target in pairs 22 | if "#{pair}#{target}" not in @curPairs 23 | @registerPair pair, target 24 | @curPairs.push("#{pair}#{target}") 25 | 26 | registerPair: (pair, target) -> 27 | [left, right] = @splitPair(pair) 28 | [target_left, target_right] = @splitPair(target) 29 | 30 | for key in [left, right] 31 | for targetKey in [target_left, target_right] 32 | if "#{key}#{targetKey}" not in @curPairsWithTarget 33 | name = "vim-surround:#{@getName(key, targetKey)}" 34 | 35 | unless pair == target 36 | @disposables.add atom.commands.add @context, name, @getRunner pair, target 37 | 38 | keymapArg = {} 39 | fullCommand = "#{@command} #{key} #{targetKey}" 40 | keymapArg[fullCommand] = name 41 | 42 | contextArg = {} 43 | contextArg[@context] = keymapArg 44 | 45 | # Capture the disposable heretom test! 46 | unless pair == target 47 | @disposables.add atom.keymaps.add name, contextArg 48 | @curPairsWithTarget.push("#{key}#{targetKey}") 49 | 50 | splitPair: (pair) -> 51 | return [pair[..(pair.length/2)-1], pair[pair.length/2..]] 52 | 53 | getRunner: (from, to) -> -> 54 | [left, right] = [from[0], from[1]] 55 | [target_left, target_right] = [to[0], to[1]] 56 | editor = atom.workspace.getActiveTextEditor() 57 | selector = new Selector(editor, left, right) 58 | 59 | editor.transact -> 60 | cursorPos = editor.getCursorBufferPosition() 61 | 62 | selector.inside().select() 63 | editor.selections.forEach (selection) -> 64 | text = selection.getText() 65 | 66 | # restore cursore and select text with surrounding keys 67 | editor.setCursorBufferPosition(cursorPos) 68 | selector.outside().select() 69 | 70 | editor.selections.forEach (selection) -> 71 | selection.insertText "#{target_left}#{text}#{target_right}" 72 | 73 | # restore cursore 74 | editor.setCursorBufferPosition(cursorPos) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim Surround for Atom [![Build Status](https://travis-ci.org/gepoch/vim-surround.svg?branch=master)](https://travis-ci.org/gepoch/vim-surround) 2 | 3 | ### NOTE: vim-surround for atom is obsoleted by vim-mode-plus. See [40](https://github.com/gepoch/vim-surround/issues/40) for more information. 4 | 5 | Surround is an implementation of vim-surround for the [atom](http://atom.io) 6 | editor, creating a vim-surround with the power of Atom! 7 | 8 | You should definitely have [vim-mode](https://atom.io/packages/vim-mode) for 9 | this package to function properly, of course. 10 | 11 | Inspiration from and kudos to the wonderful [vim-surround for 12 | vim](https://github.com/tpope/vim-surround) 13 | 14 | See vim-surround on [github](https://github.com/gepoch/vim-surround) or 15 | [atom.io](https://atom.io/packages/vim-surround). 16 | 17 | ## News 18 | 19 | * This package supports visual mode's `s )` set of commands for a configurable 20 | set of pairs. 21 | 22 | * Next on the roadmap are pair deletions with `d )` and friends. 23 | 24 | * New in 0.4: Multiple cursors are now supported, and conveniently work just 25 | like you think they do. 26 | 27 | * New in 0.5: Stable configuration changes and configurable surround key! 28 | 29 | * New in 0.7: Change surround and delete surround added. 30 | 31 | * New in 0.8: Tentative support for 32 | [vim-mode-next](https://atom.io/packages/vim-mode-next). See 33 | [#28](https://github.com/gepoch/vim-surround/issues/28). 34 | 35 | ### Muscle Memory Compatability Note 36 | 37 | vim-surround uses a lowercase `s` instead of `S` for surround commands! This is 38 | configurable in the package settings, if you would like to set it to the 39 | original keybinding. 40 | 41 | ## How to use Surround 42 | 43 | ### Surrounding 44 | 45 | For double quotes, highlight the string in visual mode and enter `s "`. 46 | 47 | ``` 48 | Hello world -> "Hello world" 49 | ``` 50 | 51 | For parentheses there are two options. `s )` will surround as normal. `s (` 52 | will pad with a space. All asymmetrical pairs have the secondary space-padded 53 | form. 54 | 55 | For example: 56 | 57 | `s )` 58 | 59 | ``` 60 | Hello world -> (Hello world) 61 | ``` 62 | 63 | `s (` 64 | 65 | ``` 66 | Hello world -> ( Hello world ) 67 | ``` 68 | 69 | ### Changing Surrounding Pairs 70 | 71 | Suppose I want to make double quotes into single quotes. To do this, I should 72 | put my cursor inside the double quotes in question and enter `c s " '` 73 | 74 | ``` 75 | "Hello world" -> 'Hello world' 76 | ``` 77 | 78 | ### Deleting Surrounding Pairs 79 | 80 | To delete the single quotes, place your cursor inside of them and enter `d s '` 81 | 82 | ``` 83 | 'Hello world' -> Hello world 84 | ``` 85 | ### Configuration 86 | 87 | Currently, the following pairs work out of the box!: 88 | 89 | - () 90 | - [] 91 | - {} 92 | - "" 93 | - '' 94 | 95 | You can add to the available pairs in atom's settings, and the commands will 96 | be dynamically added to your keybindings. 97 | 98 | For example if I'm working on Jinja templates, and I want to add the ability to 99 | surround using `{%` and `%}` I would add this in my settings: 100 | 101 | ``` 102 | (), [], {}, "", '', {%%} 103 | ``` 104 | 105 | Then: 106 | 107 | `s % }` 108 | 109 | ``` 110 | Hello world -> {%Hello world%} 111 | ``` 112 | 113 | `s { %` 114 | 115 | ``` 116 | Hello world -> {% Hello world %} 117 | ``` 118 | 119 | ### TODO 120 | 121 | - [x] Implement deleting surrounding pairs with `d s` 122 | - [x] Implement changing surrounding pairs with `c s` 123 | - [ ] Intelligent tag surrounding/deleting/replacing with `s ` and friends. 124 | --------------------------------------------------------------------------------