├── .gitignore ├── .travis.yml ├── README.md ├── UNLICENSE ├── autoload └── enmasse.vim ├── doc └── enmasse.txt ├── images └── example.gif ├── plugin └── enmasse.vim └── test ├── clearing.vader ├── enmasse.vader ├── grepable.txt ├── hints.vader ├── preview.vader ├── run ├── version.vader └── writing.vader /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | test/vader -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: vim 2 | 3 | before_script: | 4 | git clone https://github.com/junegunn/vader.vim.git 5 | 6 | script: | 7 | vim -Nu <(cat << VIMRC 8 | filetype off 9 | set rtp+=vader.vim 10 | set rtp+=. 11 | set rtp+=after 12 | filetype plugin indent on 13 | VIMRC) -c 'Vader! test/*.vader' > /dev/null -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # En Masse [![Build Status][travis-image]][travis] 2 | 3 | [![Join the chat at https://gitter.im/Wolfy87/vim-enmasse](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Wolfy87/vim-enmasse?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Takes a quickfix list and makes it editable. You can then write each change back to their respective files using your favourite way of writing files, `:w` or `ZZ`, for example. Fix multiple [JSHint][] issues at once or perform a complex find and replace across your project all within the comfort of Vim. 6 | 7 | ![Animated demonstration](./images/example.gif) 8 | 9 | ## Using the plugin 10 | 11 | As you can see in the demonstration above, all you have to do is populate a quickfix list in some way (I used [JSHint][], but you could use [Ag][], for example), then execute `:EnMasse`. This will open a new buffer with each line corresponding to a line in the quickfix list. 12 | 13 | You can then edit each line in any way you want. When done just write this magical buffer and it will update each line in their corresponding files. For more information, check out [the documentation!][docs] 14 | 15 | ## Installation 16 | 17 | ### [vim-plug](https://github.com/junegunn/vim-plug#readme) 18 | 19 | add this line to `.vimrc` 20 | 21 | ``` 22 | Plug 'Olical/vim-enmasse' 23 | ``` 24 | 25 | ### [vim-pathogen](https://github.com/tpope/vim-pathogen#readme) 26 | 27 | ``` 28 | cd ~/.vim/bundle 29 | git clone https://github.com/Olical/vim-enmasse 30 | ``` 31 | 32 | ### [Vundle.vim](https://github.com/gmarik/Vundle.vim#readme) 33 | 34 | add this line to `.vimrc` 35 | 36 | ``` 37 | Plugin 'Olical/vim-enmasse' 38 | ``` 39 | 40 | ## Tests 41 | 42 | Tests are performed using [vader][], to pull the dependencies and run them simply execute `./tests/run`. The tests are automatically executed by [TravisCI][travis] too, so keep an eye on that if you push changes or open a PR. The badge up the top of this README indicates the state of master, it should ALWAYS be green. A test should be written before any change is made. 43 | 44 | ## Author 45 | 46 | [Oliver Caldwell][author-site] ([@OliverCaldwell][author-twitter]) 47 | 48 | ## Unlicenced 49 | 50 | Find the full [unlicense][] in the `UNLICENSE` file, but here's a snippet. 51 | 52 | >This is free and unencumbered software released into the public domain. 53 | > 54 | >Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 55 | 56 | Do what you want. Learn as much as you can. Unlicense more software. 57 | 58 | [unlicense]: http://unlicense.org/ 59 | [author-site]: http://oli.me.uk/ 60 | [author-twitter]: https://twitter.com/OliverCaldwell 61 | [jshint]: https://github.com/walm/jshint.vim 62 | [ag]: https://github.com/rking/ag.vim 63 | [docs]: https://github.com/Olical/vim-enmasse/blob/master/doc/enmasse.txt 64 | [travis-image]: https://travis-ci.org/Olical/vim-enmasse.svg?branch=master 65 | [travis]: https://travis-ci.org/Olical/vim-enmasse 66 | [vader]: https://github.com/junegunn/vader.vim 67 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /autoload/enmasse.vim: -------------------------------------------------------------------------------- 1 | function! enmasse#Open() 2 | let list = s:GetQuickfixList() 3 | let sourceLines = s:GetSourceLinesFromList(list) 4 | 5 | if len(list) > 0 && len(sourceLines) > 0 6 | call s:CreateEnMasseBuffer(list, sourceLines) 7 | else 8 | call s:EchoError("No entries to edit.") 9 | endif 10 | endfunction 11 | 12 | function! enmasse#GetVersion() 13 | return "1.1.1" 14 | endfunction 15 | 16 | function! enmasse#WriteCurrentBuffer() 17 | let list = b:enMasseList 18 | let sourceLines = getline(1, "$") 19 | 20 | if len(list) ==# len(sourceLines) 21 | call s:WriteSourceLinesAgainstList(list, sourceLines) 22 | else 23 | call s:EchoError("Mismatch between buffer lines and quickfix list. Refusing to write.") 24 | endif 25 | endfunction 26 | 27 | function! enmasse#DisplayQuickfixEntryForCurrentLine() 28 | let quickfixItem = s:GetQuickfixItemForCurrentLine() 29 | call s:EchoTruncated(quickfixItem.text) 30 | endfunction 31 | 32 | function! s:EchoTruncated(msg) 33 | let saved=&shortmess 34 | set shortmess+=T 35 | exec "echomsg a:msg" 36 | let &shortmess=saved 37 | endfunction 38 | 39 | function! s:EchoError(message) 40 | echohl ErrorMsg 41 | echo "EnMasse:" a:message 42 | echohl None 43 | endfunction 44 | 45 | function! s:GetQuickfixList() 46 | let list = getqflist() 47 | let uniqueList = [] 48 | 49 | for item in list 50 | let existingItem = s:GetMatchingLineFromQuickfix(item, uniqueList) 51 | 52 | if has_key(existingItem, "bufnr") 53 | let existingItem.text = join([existingItem.text, item.text], " | ") 54 | else 55 | call add(uniqueList, item) 56 | endif 57 | endfor 58 | 59 | call sort(uniqueList, "s:SortByBufferAndLine") 60 | 61 | return uniqueList 62 | endfunction 63 | 64 | function! s:SortByBufferAndLine(i1, i2) 65 | if a:i1.bufnr > a:i2.bufnr || (a:i1.bufnr ==# a:i2.bufnr && a:i1.lnum > a:i2.lnum) 66 | return 1 67 | else 68 | return -1 69 | endif 70 | endfunction 71 | 72 | function! s:GetMatchingLineFromQuickfix(target, list) 73 | for item in a:list 74 | if a:target.bufnr ==# item.bufnr && a:target.lnum ==# item.lnum 75 | return item 76 | endif 77 | endfor 78 | 79 | return {} 80 | endfunction 81 | 82 | function! s:GetSourceLinesFromList(list) 83 | let sourceLines = [] 84 | 85 | for item in a:list 86 | let file = bufname(item.bufnr) 87 | let line = item.lnum 88 | call add(sourceLines, s:GetLineFromFile(file, line)) 89 | endfor 90 | 91 | return sourceLines 92 | endfunction 93 | 94 | function! s:GetLineFromFile(file, line) 95 | let lines = readfile(a:file, "b") 96 | return lines[a:line - 1] 97 | endfunction 98 | 99 | function! s:CreateEnMasseBuffer(list, sourceLines) 100 | noautocmd keepalt botright new! __EnMasse__ 101 | setlocal stl=\ EnMasse 102 | setlocal buftype=acwrite 103 | setlocal bufhidden=hide 104 | setlocal noswapfile 105 | setlocal nobuflisted 106 | normal! gg"_dG 107 | call setbufvar(bufnr(''), "enMasseList", a:list) 108 | call append(0, a:sourceLines) 109 | normal! "_ddgg 110 | nnoremap :call OpenLineInPreviewWindow() 111 | set nomodified 112 | if line('$') < winheight(winnr()) 113 | execute 'resize' line('$') 114 | end 115 | endfunction 116 | 117 | function! s:OpenLineInPreviewWindow() 118 | let quickfixItem = s:GetQuickfixItemForCurrentLine() 119 | let file = bufname(quickfixItem.bufnr) 120 | execute printf("pedit +%d %s", quickfixItem.lnum, file) 121 | endfunction 122 | 123 | function! s:GetQuickfixItemForCurrentLine() 124 | let list = b:enMasseList 125 | let currentLine = line(".") 126 | let quickfixItem = list[currentLine - 1] 127 | return quickfixItem 128 | endfunction 129 | 130 | function! s:WriteSourceLinesAgainstList(list, sourceLines) 131 | let toWrite = s:MergeChangesUnderPaths(a:list, a:sourceLines) 132 | 133 | for [filePath, fileChanges] in items(toWrite) 134 | let lines = readfile(filePath, "b") 135 | let changed = 0 136 | 137 | for lineChange in fileChanges 138 | if lines[lineChange.line] !=# lineChange.change 139 | let lines[lineChange.line] = lineChange.change 140 | let changed = 1 141 | endif 142 | endfor 143 | 144 | if changed 145 | execute "silent doautocmd FileWritePre " . filePath 146 | call writefile(lines, filePath, "b") 147 | execute "silent doautocmd FileWritePost " . filePath 148 | endif 149 | endfor 150 | 151 | set nomodified 152 | checktime 153 | endfunction 154 | 155 | function! s:MergeChangesUnderPaths(list, sourceLines) 156 | let index = 0 157 | let paths = {} 158 | 159 | for item in a:list 160 | let path = bufname(item.bufnr) 161 | let changes = get(paths, path, []) 162 | let paths[path] = add(changes, {"change": a:sourceLines[index], "line": item.lnum - 1}) 163 | let index += 1 164 | endfor 165 | 166 | return paths 167 | endfunction 168 | -------------------------------------------------------------------------------- /doc/enmasse.txt: -------------------------------------------------------------------------------- 1 | *enmasse.txt* 2 | 3 | ======================================================= 4 | ____ __ _ _ _ __ ____ ____ ____ 5 | ( __)( ( \ ( \/ ) / _\ / ___)/ ___)( __) 6 | ) _) / / / \/ \/ \\___ \\___ \ ) _) 7 | (____)\_)__) \_)(_/\_/\_/(____/(____/(____) 8 | 9 | Edit every file in a quickfix list at the same time. 10 | ======================================================= 11 | 12 | =============================================================================== 13 | Introduction *enmasse* *enmasse-introduction* 14 | 15 | Takes a |quickfix| list and makes it editable. You can then write each change 16 | back to their respective files using your favourite way of writing files, |:w| 17 | or |ZZ|, for example. Fix multiple linting issues at once or perform a complex 18 | find and replace across your project all within the comfort of Vim. 19 | 20 | =============================================================================== 21 | Usage *enmasse-usage* 22 | 23 | All you have to do is populate a quickfix list in some way (using JSHint or Ag, 24 | for example), then execute :EnMasse. This will open a new buffer with each line 25 | corresponding to a line in the quickfix list. 26 | 27 | You can then edit each line in any way you want. When done just write this 28 | magical buffer and it will update each line in their corresponding files. Do 29 | not delete or create any new lines, that will not work, EnMasse will prevent 30 | you from writing if it spots a discrepancy because it no longer knows which 31 | lines should go where. 32 | 33 | Pressing enter on a line will open the preview window to that line so you can 34 | get the context of what you're about to edit. This mimics the functionality of 35 | the quickfix list. 36 | 37 | =============================================================================== 38 | Autocommands *enmasse-autocommands* 39 | 40 | When writing changes to files, EnMasse will batch writes together. That means 41 | that if you have multiple changes for one file, only one write will take place. 42 | With this in mind, EnMasse will fire |FileWritePre| and |FileWritePost| for 43 | each file that is changed. 44 | 45 | =============================================================================== 46 | Quickfix hints *enmasse-quickfix-hints* 47 | 48 | As you move your cursor through the lines the matching quickfix entry message 49 | will be echoed at the bottom of the screen. So if you're scrolling through a 50 | buffer created from a JSHint quickfix list, you'll be provided with the 51 | corresponding JSHint message for each line at the bottom of the window. 52 | 53 | If there were multiple quickfix entries for a single line (missing semi-colon 54 | and unused variable, for example) then their messages will be merged into one 55 | in the hint. If the message is too long to fit on one line it will be 56 | truncated. It's either that or you have a "press enter to continue" prompt pop 57 | up every time the echo wraps onto the next line. Not cool. So truncation is the 58 | better alternative, even if you lose a bit of information sometimes. 59 | 60 | =============================================================================== 61 | Author *enmasse-author* 62 | 63 | Oliver Caldwell / @OliverCaldwell 64 | 65 | =============================================================================== 66 | Unlicence *enmasse-unlicence* 67 | 68 | This is free and unencumbered software released into the public domain. For 69 | more information, please refer to or the "README" of 70 | this project. 71 | 72 | vim:tw=78:sw=4:ts=4:ft=help:norl: 73 | -------------------------------------------------------------------------------- /images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olical/vim-enmasse/c2286f1d7bd735287a661cd223cd94e2a1f74deb/images/example.gif -------------------------------------------------------------------------------- /plugin/enmasse.vim: -------------------------------------------------------------------------------- 1 | command! EnMasse :call enmasse#Open() 2 | command! EnMasseVersion :echo enmasse#GetVersion() 3 | 4 | augroup EnMasseDefault 5 | autocmd! 6 | autocmd WinLeave __EnMasse__ wincmd p 7 | autocmd BufWriteCmd __EnMasse__ call enmasse#WriteCurrentBuffer() 8 | autocmd CursorMoved __EnMasse__ call enmasse#DisplayQuickfixEntryForCurrentLine() 9 | augroup END 10 | -------------------------------------------------------------------------------- /test/clearing.vader: -------------------------------------------------------------------------------- 1 | Execute (the line counts are correct): 2 | silent grep! -i a test/grepable.txt 3 | EnMasse 4 | let before = line("$") 5 | normal G 6 | quit 7 | silent grep! -i b test/grepable.txt 8 | EnMasse 9 | let after = line("$") 10 | 11 | AssertEqual 5, before 12 | AssertEqual 2, after 13 | -------------------------------------------------------------------------------- /test/enmasse.vader: -------------------------------------------------------------------------------- 1 | Before (read the example grepable file and grep for quickfix): 2 | let lines = readfile("test/grepable.txt", "b") 3 | silent grep! quickfix test/grepable.txt 4 | 5 | Execute (can't call :EnMasse without a quickfix list): 6 | call setqflist([]) 7 | 8 | redir => messages 9 | EnMasse 10 | redir END 11 | 12 | let result = get(split(messages, "\n"), -1, "") 13 | 14 | AssertEqual "EnMasse: No entries to edit.", result 15 | 16 | Execute (:EnMasse with a quickfix list creates a buffer): 17 | EnMasse 18 | let name = bufname("%") 19 | quit 20 | 21 | AssertEqual "__EnMasse__", name 22 | 23 | Execute (the buffer contains the correct line from the quickfix list): 24 | EnMasse 25 | let firstLine = getline("1") 26 | quit 27 | 28 | AssertEqual lines[1], firstLine 29 | 30 | Execute (duplicate quickfix lines are joined together): 31 | silent grepadd! loaded test/grepable.txt 32 | EnMasse 33 | let firstLine = getline("1") 34 | let secondLine = getline("2") 35 | let lineCount = line("$") 36 | quit 37 | 38 | AssertEqual lines[1], firstLine 39 | AssertEqual lines[3], secondLine 40 | AssertEqual 2, lineCount 41 | -------------------------------------------------------------------------------- /test/grepable.txt: -------------------------------------------------------------------------------- 1 | This is an example file. 2 | It should be loaded into the quickfix list. 3 | Then you can run :EnMasse 4 | And edit that quickfix list. 5 | And this line is way too long so it will truncated. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et ornare libero, quis vulputate odio. In luctus id velit sed gravida. Cras turpis nibh, luctus a lorem non, sollicitudin auctor justo. Vivamus tristique dolor a lectus gravida molestie. Nunc varius mi ante, vitae bibendum quam ultricies vitae. 6 | Dupe! 7 | -------------------------------------------------------------------------------- /test/hints.vader: -------------------------------------------------------------------------------- 1 | Before (read the example grepable file and grep for quickfix): 2 | let lines = readfile("test/grepable.txt", "b") 3 | silent grep! quickfix test/grepable.txt 4 | 5 | Execute (when the cursor is moved, the hint updates): 6 | EnMasse 7 | normal j 8 | 9 | redir => messages 10 | doautocmd CursorMoved 11 | redir END 12 | 13 | quit 14 | let result = get(split(messages, "\n"), -1, "") 15 | 16 | AssertEqual lines[3], result 17 | 18 | Execute (when lines have been joined, the hint contains both of the results seperated with a pipe): 19 | silent grep! "Dupe!" test/grepable.txt 20 | silent grepadd! "Dupe!" test/grepable.txt 21 | EnMasse 22 | normal jk 23 | 24 | redir => messages 25 | doautocmd CursorMoved 26 | redir END 27 | 28 | quit 29 | let result = get(split(messages, "\n"), -1, "") 30 | 31 | AssertEqual join([lines[5], lines[5]], " | "), result -------------------------------------------------------------------------------- /test/preview.vader: -------------------------------------------------------------------------------- 1 | Before (set up an EnMasse buffer): 2 | let lines = readfile("test/grepable.txt", "b") 3 | silent grep! quickfix test/grepable.txt 4 | EnMasse 5 | 6 | Execute (hitting enter on a line opens it in the preview window): 7 | let before = getline(".") 8 | execute "normal \\k" 9 | let after = getline(".") 10 | let bufferName = expand("%") 11 | AssertEqual "test/grepable.txt", bufferName 12 | AssertEqual before, after 13 | pclose -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -d test/vader ]]; then 4 | git clone https://github.com/junegunn/vader.vim.git test/vader 5 | else 6 | pushd test/vader 7 | git pull 8 | popd 9 | fi 10 | 11 | vim -Nu <(cat << VIMRC 12 | filetype off 13 | set rtp=. 14 | set rtp+=test/vader 15 | filetype plugin indent on 16 | VIMRC 17 | ) -c 'Vader! test/*.vader' > /dev/null -------------------------------------------------------------------------------- /test/version.vader: -------------------------------------------------------------------------------- 1 | Before (set up regular expression): 2 | let versionRegExp = '\v\d+\.\d+\.\d+' 3 | 4 | Execute (can print the version number with the command): 5 | redir => messages 6 | EnMasseVersion 7 | redir END 8 | 9 | let result = get(split(messages, "\n"), -1, "") 10 | 11 | Assert result =~# versionRegExp 12 | 13 | Execute (can get the version number with the function): 14 | Assert enmasse#GetVersion() =~# versionRegExp -------------------------------------------------------------------------------- /test/writing.vader: -------------------------------------------------------------------------------- 1 | Before (define test data and setup EnMasse): 2 | let original = ["EnMasse is cOoL", "Hello, World!", "This is EnMasse.", "EnMasse is useful."] 3 | let expected = ["EnMasse is Cool", "Hello, World!", "This is EnMasse, a Vim plugin.", "EnMasse is handy."] 4 | let filePath = tempname() 5 | set nomodified 6 | call writefile(original, filePath) 7 | execute "silent grep! EnMasse " . filePath 8 | EnMasse 9 | 10 | After (remove the temporary file and close the previous EnMasse buffer): 11 | call delete(filePath) 12 | quit! 13 | 14 | Execute (editing and writing in an EnMasse buffer changes the file): 15 | %s/is EnMasse/is EnMasse, a Vim plugin/ 16 | %s/useful/handy/ 17 | %s/cOoL/Cool/ 18 | set ignorecase 19 | write 20 | set noignorecase 21 | let actual = readfile(filePath) 22 | AssertEqual expected, actual 23 | 24 | Execute (will not let you write if a line is deleted): 25 | normal dd 26 | redir => messages 27 | write 28 | redir END 29 | let actual = readfile(filePath) 30 | let latestMessage = get(split(messages, "\n"), -1, "") 31 | AssertEqual original, actual 32 | AssertEqual "EnMasse: Mismatch between buffer lines and quickfix list. Refusing to write.", latestMessage 33 | 34 | Execute (will not let you write if a line is added): 35 | normal o 36 | redir => messages 37 | write 38 | redir END 39 | let actual = readfile(filePath) 40 | let latestMessage = get(split(messages, "\n"), -1, "") 41 | AssertEqual original, actual 42 | AssertEqual "EnMasse: Mismatch between buffer lines and quickfix list. Refusing to write.", latestMessage 43 | 44 | Execute (doesn't write if no lines have changed): 45 | let before = getftime(filePath) 46 | write 47 | let after = getftime(filePath) 48 | AssertEqual before, after 49 | 50 | Execute (changing a file that you have open will prompt for a reload): 51 | execute "split " . filePath 52 | %s/is EnMasse/is EnMasse, a Vim plugin/ 53 | %s/useful/handy/ 54 | write 55 | execute "normal! l\\j" 56 | let bufferLines = getline(1, "$") 57 | let actual = readfile(filePath) 58 | AssertEqual actual, bufferLines 59 | 60 | Execute (multiple changes to one file are written in one batched write): 61 | %s/is EnMasse/is EnMasse, a Vim plugin/ 62 | %s/useful/handy/ 63 | let writes = 0 64 | let actual = readfile(filePath) 65 | autocmd FileWritePost * let writes += 1 66 | write 67 | AssertEqual 1, writes 68 | --------------------------------------------------------------------------------