├── .gitignore ├── .rspec ├── Gemfile ├── assets ├── example1.gif ├── example2.gif ├── example3.gif └── example4.gif ├── .travis.yml ├── Rakefile ├── CONTRIBUTING.md ├── Gemfile.lock ├── spec ├── spec_helper.rb ├── benchmark_spec.rb └── multiple_cursors_spec.rb ├── MIT-LICENSE.txt ├── CHANGELOG.md ├── plugin └── multiple_cursors.vim ├── doc └── multiple_cursors.txt ├── README.md └── autoload └── multiple_cursors.vim /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format d 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'vimrunner' 3 | gem 'rake' 4 | gem 'rspec' 5 | -------------------------------------------------------------------------------- /assets/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terryma/vim-multiple-cursors/HEAD/assets/example1.gif -------------------------------------------------------------------------------- /assets/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terryma/vim-multiple-cursors/HEAD/assets/example2.gif -------------------------------------------------------------------------------- /assets/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terryma/vim-multiple-cursors/HEAD/assets/example3.gif -------------------------------------------------------------------------------- /assets/example4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terryma/vim-multiple-cursors/HEAD/assets/example4.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | language: ruby 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - vim-gtk 9 | - xvfb 10 | 11 | script: 12 | - xvfb-run bundle exec rake 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) do |t| 4 | t.pattern = 'spec/multiple_cursors_spec.rb' 5 | end 6 | 7 | RSpec::Core::RakeTask.new(:benchmark) do |t| 8 | t.pattern = 'spec/benchmark_spec.rb' 9 | end 10 | 11 | task :default => :spec 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Problems summary 2 | 3 | ## Expected 4 | 5 | ## Environment Information 6 | * OS: 7 | * Neovim/Vim/Gvim version: 8 | 9 | ## Provide a minimal .vimrc with less than 50 lines 10 | 11 | " Your minimal.vimrc 12 | 13 | ## Generate a logfile if appropriate 14 | 15 | 1. export NVIM_PYTHON_LOG_FILE=/tmp/log 16 | 2. export NVIM_PYTHON_LOG_LEVEL=DEBUG 17 | 3. nvim -u minimal.vimrc 18 | 4. recreate your issue 19 | 5. cat /tmp/log_{PID} 20 | 21 | ## Screen shot (if possible) 22 | 23 | ## Upload the log file 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | rake (10.4.2) 6 | rspec (3.4.0) 7 | rspec-core (~> 3.4.0) 8 | rspec-expectations (~> 3.4.0) 9 | rspec-mocks (~> 3.4.0) 10 | rspec-core (3.4.1) 11 | rspec-support (~> 3.4.0) 12 | rspec-expectations (3.4.0) 13 | diff-lcs (>= 1.2.0, < 2.0) 14 | rspec-support (~> 3.4.0) 15 | rspec-mocks (3.4.0) 16 | diff-lcs (>= 1.2.0, < 2.0) 17 | rspec-support (~> 3.4.0) 18 | rspec-support (3.4.1) 19 | vimrunner (0.3.1) 20 | 21 | PLATFORMS 22 | ruby 23 | 24 | DEPENDENCIES 25 | rake 26 | rspec 27 | vimrunner 28 | 29 | BUNDLED WITH 30 | 1.10.6 31 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'vimrunner' 2 | require 'vimrunner/rspec' 3 | 4 | Vimrunner::RSpec.configure do |config| 5 | 6 | # Use a single Vim instance for the test suite. Set to false to use an 7 | # instance per test (slower, but can be easier to manage). 8 | config.reuse_server = false 9 | 10 | # Decide how to start a Vim instance. In this block, an instance should be 11 | # spawned and set up with anything project-specific. 12 | config.start_vim do 13 | # vim = Vimrunner.start 14 | 15 | # Or, start a GUI instance: 16 | vim = Vimrunner.start_gvim 17 | 18 | # Setup your plugin in the Vim instance 19 | plugin_path = File.expand_path('../..', __FILE__) 20 | vim.add_plugin(plugin_path, 'plugin/multiple_cursors.vim') 21 | 22 | # The returned value is the Client available in the tests. 23 | vim 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Terry Ma 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 included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2 (06/10/2013) 2 | Bugfixes: 3 | - Fix plugin break in PASTE mode. This fixes #44. 4 | 5 | ## 2.1 (04/26/2013) 6 | 7 | Bugfixes: 8 | - Fix 1 regression where cursors could potentially get out of sync in insert mode 9 | 10 | Features: 11 | - Added some logic to debug latency. Fanning out to 30 cursors in insert mode with my vimrc took over 300ms. It's like than 20ms with a plain vimrc. Need to debug what setting is causing the slowing down in insert mode and inform users. 12 | 13 | ## 2.0 (04/24/2013) 14 | 15 | Bugfixes: 16 | - Fix inconsistent undo behavior. Changes made in multicursor insert mode are now undone together. This fixes #22. 17 | - Single key commands that do not terminate properly no longer cause ghostly cursors to linger on screen. An error message is now displayed informing the user the number of cursor locations that the input cannot be properly played back at. This fixes #28. 18 | 19 | ## 1.16 (04/23/2013) 20 | 21 | Features: 22 | - Add integration tests using vimrunner. Hook up travis-ci to run continous integration on commit. 23 | 24 | ## 1.15 (04/22/2013) 25 | 26 | Bugfixes: 27 | - Fix plugin causing error bell. This fixes #29. 28 | 29 | ## 1.14 (04/22/2013) 30 | 31 | Features: 32 | - Allow users to separate start key from next key. (credit: @xanderman) 33 | 34 | ## 1.13 (04/22/2013) 35 | 36 | Bugfixes: 37 | - Add support for switching to visual line mode from inside multicursor mode 38 | - Fix highlight issue where extra character at end of line is highlighted for visual selections covering more than 2 lines. 39 | 40 | ## 1.12 (04/19/2013) 41 | 42 | Bugfixes: 43 | - Fix tab character causing highlight errors. This fixes #18 and fixes #32 44 | 45 | ## 1.11 (04/18/2013) 46 | 47 | Bugfixes: 48 | - Fix regression where `C-n` doesn't exhibit correct behavior when all matches have been found 49 | - Clear echo messages when a new input is received 50 | 51 | ## 1.10 (04/17/2013) 52 | 53 | Bugfixes: 54 | - `O` works now in normal mode. This fixes #24 55 | - Turn on `lazyredraw` during multicursor mode to prevent the sluggish screen redraws 56 | 57 | Features: 58 | - Add command **MultipleCursorsFind** to add multiple virtual cursors using regexp. This closes #20 59 | 60 | ## 1.9 (04/17/2013) 61 | 62 | Bugfixes: 63 | - Fix starting multicursor mode in visual line mode. This fixes #25 64 | - Major refactoring to avoid getting in and out of visual mode as much as possible 65 | 66 | ## 1.8 (04/16/2013) 67 | 68 | Bugfixes: 69 | - Fix regression that causes call stack to explode with too many cursors 70 | 71 | ## 1.7 (04/15/2013) 72 | 73 | Bugfixes: 74 | - Finally fix the annoying highlighting problem when the last virtual cursor is on the last character of the line. The solution is a hack, but it should be harmless 75 | 76 | ## 1.6 (04/15/2013) 77 | 78 | Bugfixes: 79 | - Stop chaining dictionary function calls. This fixes #10 and #11 80 | 81 | ## 1.5 (04/15/2013) 82 | 83 | Bugfixes: 84 | - Exit Vim's visual mode before waiting for user's next input. This fixes #14 85 | 86 | ## 1.4 (04/14/2013) 87 | 88 | Bugfixes: 89 | - Don't use clearmatches(). It clears highlighting from other plugins. This fixes #13 90 | 91 | ## 1.3 (04/14/2013) 92 | 93 | Bugfixes: 94 | - Change mapping from using expression-quote syntax to using raw strings 95 | 96 | ## 1.2 (04/14/2013) 97 | 98 | Bugfixes: 99 | - Restore view when exiting from multicursor mode. This fixes #5 100 | - Remove the unnecessary user level mapping for 'prev' and 'skip' in visual mode, since we can purely detect those keys from multicursor mode 101 | 102 | ## 1.1 (04/14/2013) 103 | 104 | Bugfixes: 105 | - Stop hijacking escape key in normal mode. This fixes #1, #2, and #3 106 | 107 | ## 1.0 (04/13/2013) 108 | 109 | Initial release 110 | -------------------------------------------------------------------------------- /plugin/multiple_cursors.vim: -------------------------------------------------------------------------------- 1 | "=============================================================================== 2 | " File: multiple_cursors.vim 3 | " Author: Terry Ma 4 | " Description: Emulate Sublime Text's multi selection feature 5 | " Potential Features: 6 | " - Create a blinking cursor effect? Good place to do it would be instead of 7 | " waiting for user input, cycle through the highlight 8 | " - Integrate with the status line? Maybe show a special multicursor mode? 9 | " - Support mouse? Ctrl/Cmd click to set cursor? 10 | "=============================================================================== 11 | let s:save_cpo = &cpo 12 | set cpo&vim 13 | 14 | function! s:init_settings(settings) 15 | for [key, value] in items(a:settings) 16 | let sub = '' 17 | if type(value) == 0 18 | let sub = '%d' 19 | elseif type(value) == 1 20 | let sub = '"%s"' 21 | endif 22 | let fmt = printf("let g:multi_cursor_%%s=get(g:, 'multi_cursor_%%s', %s)", 23 | \ sub) 24 | exec printf(fmt, key, key, value) 25 | endfor 26 | endfunction 27 | 28 | " Settings 29 | let s:settings = { 30 | \ 'exit_from_visual_mode': 0, 31 | \ 'exit_from_insert_mode': 0, 32 | \ 'use_default_mapping': 1, 33 | \ 'debug_latency': 0, 34 | \ 'support_imap': 1, 35 | \ } 36 | 37 | let s:settings_if_default = { 38 | \ 'quit_key': '', 39 | \ 'start_key': 'g', 40 | \ 'start_word_key': '', 41 | \ 'next_key': '', 42 | \ 'prev_key': '', 43 | \ 'skip_key': '', 44 | \ 'select_all_key': 'g', 45 | \ 'select_all_word_key': '', 46 | \ } 47 | 48 | let s:default_normal_maps = {'!':1, '@':1, '=':1, 'q':1, 'r':1, 't':1, 'T':1, 'y':1, '[':1, ']':1, '\':1, 'd':1, 'f':1, 'F':1, 'g':1, '"':1, 'z':1, 'c':1, 'm':1, '<':1, '>':1} 49 | let s:default_visual_maps = {'i':1, 'a':1, 'f':1, 'F':1, 't':1, 'T':1} 50 | 51 | let g:multi_cursor_normal_maps = 52 | \ get(g:, 'multi_cursor_normal_maps', s:default_normal_maps) 53 | let g:multi_cursor_visual_maps = 54 | \ get(g:, 'multi_cursor_visual_maps', s:default_visual_maps) 55 | 56 | call s:init_settings(s:settings) 57 | 58 | if g:multi_cursor_use_default_mapping 59 | call s:init_settings(s:settings_if_default) 60 | endif 61 | 62 | if !exists('g:multi_cursor_start_word_key') 63 | if exists('g:multi_cursor_next_key') 64 | let g:multi_cursor_start_word_key = g:multi_cursor_next_key 65 | endif 66 | endif 67 | 68 | " External mappings 69 | if exists('g:multi_cursor_start_key') 70 | exec 'nnoremap '.g:multi_cursor_start_key. 71 | \' :call multiple_cursors#new("n", 0)' 72 | exec 'xnoremap '.g:multi_cursor_start_key. 73 | \' :call multiple_cursors#new("v", 0)' 74 | endif 75 | 76 | if exists('g:multi_cursor_start_word_key') 77 | exec 'nnoremap '.g:multi_cursor_start_word_key. 78 | \' :call multiple_cursors#new("n", 1)' 79 | " In Visual mode word boundary is not used 80 | exec 'xnoremap '.g:multi_cursor_start_word_key. 81 | \' :call multiple_cursors#new("v", 0)' 82 | endif 83 | 84 | if exists('g:multi_cursor_select_all_key') 85 | exec 'nnoremap '.g:multi_cursor_select_all_key. 86 | \' :call multiple_cursors#select_all("n", 0)' 87 | exec 'xnoremap '.g:multi_cursor_select_all_key. 88 | \' :call multiple_cursors#select_all("v", 0)' 89 | endif 90 | 91 | if exists('g:multi_cursor_select_all_word_key') 92 | exec 'nnoremap '.g:multi_cursor_select_all_word_key. 93 | \' :call multiple_cursors#select_all("n", 1)' 94 | " In Visual mode word boundary is not used 95 | exec 'xnoremap '.g:multi_cursor_select_all_word_key. 96 | \' :call multiple_cursors#select_all("v", 0)' 97 | endif 98 | 99 | " Commands 100 | command! -nargs=1 -range=% MultipleCursorsFind 101 | \ call multiple_cursors#find(, , ) 102 | 103 | let &cpo = s:save_cpo 104 | unlet s:save_cpo 105 | -------------------------------------------------------------------------------- /spec/benchmark_spec.rb: -------------------------------------------------------------------------------- 1 | require 'vimrunner' 2 | require 'vimrunner/rspec' 3 | 4 | Vimrunner::RSpec.configure do |config| 5 | 6 | # Use a single Vim instance for the test suite. Set to false to use an 7 | # instance per test (slower, but can be easier to manage). 8 | config.reuse_server = false 9 | 10 | # Decide how to start a Vim instance. In this block, an instance should be 11 | # spawned and set up with anything project-specific. 12 | config.start_vim do 13 | # vim = Vimrunner.start 14 | # vim = Vimrunner::Server.new("/usr/local/bin/vim").start 15 | 16 | # Or, start a GUI instance: 17 | vim = Vimrunner.start_gvim 18 | 19 | # Setup your plugin in the Vim instance 20 | plugin_path = File.expand_path('../..', __FILE__) 21 | vim.add_plugin(plugin_path, 'plugin/multiple_cursors.vim') 22 | 23 | # The returned value is the Client available in the tests. 24 | vim 25 | end 26 | end 27 | 28 | def set_file_content(string) 29 | string = normalize_string_indent(string) 30 | File.open(filename, 'w'){ |f| f.write(string) } 31 | vim.edit filename 32 | end 33 | 34 | def get_file_content() 35 | vim.write 36 | IO.read(filename).strip 37 | end 38 | 39 | def before(string) 40 | set_file_content(string) 41 | end 42 | 43 | def after(string) 44 | get_file_content().should eq normalize_string_indent(string) 45 | type ":q" 46 | end 47 | 48 | def type(string) 49 | string.scan(/<.*?>|./).each do |key| 50 | if /<.*>/.match(key) 51 | vim.feedkeys "\\#{key}" 52 | else 53 | vim.feedkeys key 54 | end 55 | end 56 | sleep 0.2 57 | end 58 | 59 | describe "Multiple Cursors" do 60 | let(:filename) { 'test.txt' } 61 | let(:options) { [] } 62 | 63 | specify "#benchmark" do 64 | before <<-EOF 65 | hello 66 | hello 67 | hello 68 | hello 69 | hello 70 | hello 71 | hello 72 | hello 73 | hello 74 | hello 75 | hello 76 | hello 77 | hello 78 | hello 79 | hello 80 | hello 81 | hello 82 | hello 83 | hello 84 | hello 85 | hello 86 | hello 87 | hello 88 | hello 89 | hello 90 | hello 91 | hello 92 | hello 93 | hello 94 | hello 95 | EOF 96 | 97 | # type ':profile start /tmp/test.result' 98 | # type ':profile! file *multiple_cursors.vim' 99 | type ':let g:multi_cursor_debug_latency=1' 100 | 101 | type 'VGVchellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello' 102 | 103 | type ':echo multiple_cursors#get_latency_debug_file()' 104 | sleep 3 105 | latency_file = vim.command 'echo multiple_cursors#get_latency_debug_file()' 106 | puts 'latency file = ' + latency_file 107 | 108 | after <<-EOF 109 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 110 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 111 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 112 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 113 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 114 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 115 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 116 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 117 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 118 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 119 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 120 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 121 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 122 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 123 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 124 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 125 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 126 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 127 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 128 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 129 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 130 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 131 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 132 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 133 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 134 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 135 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 136 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 137 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 138 | hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 139 | EOF 140 | end 141 | 142 | end 143 | -------------------------------------------------------------------------------- /doc/multiple_cursors.txt: -------------------------------------------------------------------------------- 1 | *vim-multiple-cursors.txt* True Sublime Text multiple selection in Vim 2 | 3 | ____ _ __ 4 | ____ ___ __ __/ / /_(_)___ / /__ _______ ________________ __________ 5 | / __ `__ \/ / / / / __/ / __ \/ / _ \ / ___/ / / / ___/ ___/ __ \/ ___/ ___/ 6 | / / / / / / /_/ / / /_/ / /_/ / / __/ / /__/ /_/ / / (__ ) /_/ / / (__ ) 7 | /_/ /_/ /_/\__,_/_/\__/_/ .___/_/\___/ \___/\__,_/_/ /____/\____/_/ /____/ 8 | /_/ 9 | 10 | 11 | Reference Manual~ 12 | 13 | 14 | ============================================================================== 15 | 16 | CONTENTS *multiple-cursors-contents* 17 | 1.Intro...................................|multiple-cursors-intro| 18 | 2.Usage...................................|multiple-cursors-usage| 19 | 3.Mappings................................|multiple-cursors-mappings| 20 | 4.Global Options..........................|multiple-cursors-global-options| 21 | 5.Interactions with other plugins.........|multiple-cursors-other-plugins| 22 | 6.Highlight...............................|multiple-cursors-highlight| 23 | 7.FAQ.....................................|multiple-cursors-faq| 24 | 8.Contributing............................|multiple-cursors-contributing| 25 | 9.License.................................|multiple-cursors-license| 26 | 10.Credit..................................|multiple-cursors-credit| 27 | 11.References..............................|multiple-cursors-references| 28 | 29 | ============================================================================== 30 | 1. Intro *multiple-cursors-intro* 31 | 32 | There [1] have [2] been [3] many [4] attempts [5] at bringing Sublime Text's 33 | awesome multiple selection [6] feature into Vim, but none so far have been in 34 | my opinion a faithful port that is simplistic to use, yet powerful and 35 | intuitive enough for an existing Vim user. *vim-multiple-cursors* is yet 36 | another attempt at that. 37 | 38 | ============================================================================== 39 | 2. Usage *multiple-cursors-usage* 40 | 41 | normal mode / visual mode~ 42 | 43 | - start: `` start multicursor and add a virtual cursor + visual selection on the match 44 | + next: `` add a new virtual cursor + visual selection on the next match 45 | + skip: `` skip the next match 46 | + prev: `` remove current virtual cursor + visual selection and go back on previous match 47 | - select all: `` start muticursor and directly select all matches 48 | 49 | You can now change the virtual cursors + visual selection with |visual-mode| commands. 50 | For instance: `c`, `s`, `I`, `A` work without any issues. 51 | You could also go to |normal-mode| by pressing `v` and use normal commands there. 52 | 53 | At any time, you can press `` to exit back to regular Vim. 54 | 55 | NOTE: start with `g` to match without boundaries (behaves like `g*` instead of `*`, see |gstar|) 56 | 57 | visual mode when multiple lines are selected~ 58 | 59 | - start: `` add virtual cursors on each line 60 | 61 | You can now change the virtual cursors with |normal-mode| commands. 62 | For instance: `ciw`. 63 | 64 | command~ 65 | 66 | The command `MultipleCursorsFind` accepts a range and a pattern (regexp), it 67 | creates a visual cursor at the end of each match. 68 | If no range is passed in, then it defaults to the entire buffer. 69 | 70 | ============================================================================== 71 | 3. Mappings *multiple-cursors-mappings* 72 | 73 | If you don't like the plugin taking over your favorite key bindings, you can 74 | turn off the default with > 75 | 76 | let g:multi_cursor_use_default_mapping=0 77 | 78 | " Default mapping 79 | let g:multi_cursor_start_word_key = '' 80 | let g:multi_cursor_select_all_word_key = '' 81 | let g:multi_cursor_start_key = 'g' 82 | let g:multi_cursor_select_all_key = 'g' 83 | let g:multi_cursor_next_key = '' 84 | let g:multi_cursor_prev_key = '' 85 | let g:multi_cursor_skip_key = '' 86 | let g:multi_cursor_quit_key = '' 87 | < 88 | 89 | NOTE: Please make sure to always map something to |g:multi_cursor_quit_key|, 90 | otherwise you'll have a tough time quitting from multicursor mode. 91 | 92 | ============================================================================== 93 | 4. Global Options *multiple-cursors-global-options* 94 | 95 | Currently there are four additional global settings one can tweak: 96 | 97 | *g:multi_cursor_support_imap* (Default: 1) 98 | 99 | If set to 0, insert mappings won't be supported in |insert-mode| anymore. 100 | 101 | *g:multi_cursor_exit_from_visual_mode* (Default: 0) 102 | 103 | If set to 0, then pressing |g:multi_cursor_quit_key| in |visual-mode| will quit 104 | and delete all existing cursors, skipping normal mode with multiple cursors. 105 | 106 | *g:multi_cursor_exit_from_insert_mode* (Default: 0) 107 | 108 | If set to 1, then pressing |g:multi_cursor_quit_key| in |insert-mode| will quit 109 | and delete all existing cursors, skipping normal mode with multiple cursors. 110 | 111 | *g:multi_cursor_normal_maps* (Default: see below) 112 | 113 | `{'@': 1, 'F': 1, 'T': 1, '[': 1, '\': 1, ']': 1, '!': 1, '"': 1, 'c': 1, 'd': 1, 'f': 1, 'g': 1, 'm': 1, 'q': 1, 'r': 1, 't': 1, 'y': 1, 'z': 1, '<': 1, '=': 1, '>': 1}` 114 | 115 | Any key in this map (values are ignored) will cause multi-cursor _Normal_ mode 116 | to pause for map completion just like normal vim. Otherwise keys mapped in 117 | normal mode will "fail to replay" when multiple cursors are active. For 118 | example: `{'d':1}` makes normal-mode command `dw` work in multi-cursor mode. 119 | 120 | The default list contents should work for anybody, unless they have remapped a 121 | key from an operator-pending command to a non-operator-pending command or 122 | vice versa. 123 | 124 | These keys must be manually listed because vim doesn't provide a way to 125 | automatically see which keys _start_ mappings, and trying to run motion commands 126 | such as `j` as if they were operator-pending commands can break things. 127 | 128 | *g:multi_cursor_visual_maps* (Default: ) 129 | 130 | `{'T': 1, 'a': 1, 't': 1, 'F': 1, 'f': 1, 'i': 1}` 131 | 132 | Same principle as |g:multi_cursor_normal_maps| 133 | 134 | ============================================================================== 135 | 5. Interactions with other plugins *multiple-cursors-other-plugins* 136 | 137 | Other plugins may be incompatible in insert mode. That is why we provide 138 | hooks to disable those plug-ins when vim-multiple-cursors is active: 139 | 140 | For example, if you are using `Neocomplete`, add this to your vimrc to prevent 141 | conflict: 142 | > 143 | function! Multiple_cursors_before() 144 | if exists(':NeoCompleteLock')==2 145 | exe 'NeoCompleteLock' 146 | endif 147 | endfunction 148 | 149 | function! Multiple_cursors_after() 150 | if exists(':NeoCompleteUnlock')==2 151 | exe 'NeoCompleteUnlock' 152 | endif 153 | endfunction 154 | 155 | Plugins themselves can register |User| |autocommand| on `MultipleCursorsPre` and 156 | `MultipleCursorsPost` for automatic integration. 157 | 158 | ============================================================================== 159 | 6. Highlight *multiple-cursors-highlight* 160 | > 161 | The plugin uses the highlight group `multiple_cursors_cursor` and 162 | `multiple_cursors_visual` to highlight the virtual cursors and their visual 163 | selections respectively. You can customize them by putting something similar 164 | like the following in your vimrc: > 165 | " Default highlighting (see help :highlight and help :highlight-link) 166 | highlight multiple_cursors_cursor term=reverse cterm=reverse gui=reverse 167 | highlight link multiple_cursors_visual Visual 168 | 169 | ============================================================================== 170 | 7. FAQ *multiple-cursors-faq* 171 | 172 | Q: Pressing after selecting words with makes the plugin hang, why? 173 | A: When selecting words with , the plugin behaves like in `visual` mode. 174 | Once you pressed , you can still press to insert text. 175 | 176 | Q: doesn't seem to work in VIM but works in gVIM, why? 177 | A: This is a well known terminal/Vim [9], different terminal have different 178 | ways to send `Alt+key`. Try adding this in your `.vimrc` and make sure 179 | to replace the string: > 180 | if !has('gui_running') 181 | map "in Insert mode, type Ctrl+v Alt+n here" 182 | endif 183 | Or remap the following: > 184 | g:multi_cursor_start_key 185 | g:multi_cursor_select_all_key 186 | 187 | Q: doesn't seem to work in gVIM? 188 | A: Try setting `set selection=inclusive` in your `~/.gvimrc` 189 | 190 | Q: deoplete insert giberrish, how to fix this? 191 | A: use the `Multiple_cursors` functions, add this in your vimrc: > 192 | func! Multiple_cursors_before() 193 | if deoplete#is_enabled() 194 | call deoplete#disable() 195 | let g:deoplete_is_enable_before_multi_cursors = 1 196 | else 197 | let g:deoplete_is_enable_before_multi_cursors = 0 198 | endif 199 | endfunc 200 | func! Multiple_cursors_after() 201 | if g:deoplete_is_enable_before_multi_cursors 202 | call deoplete#enable() 203 | endif 204 | endfunc 205 | 206 | Q: is it also working on Mac? 207 | A: On Mac OS, MacVim[10] is known to work. 208 | 209 | Q: How can I select `n` keywords with several keystrokes? `200` does not work. 210 | A: You can use :MultipleCursorsFind keyword. I have this binding in my vimrc: > 211 | nnoremap :MultipleCursorsFind / 212 | vnoremap :MultipleCursorsFind / 213 | This allows one to search for the keyword using `*` and turn search results into cursors with `Alt-j`. 214 | 215 | ============================================================================== 216 | 8. Contributing *multiple-cursors-contributing* 217 | 218 | The project is hosted on Github. Patches, feature requests and suggestions are 219 | always welcome! 220 | 221 | Find the latest version of the plugin here: 222 | http://github.com/terryma/vim-multiple-cursors 223 | 224 | ============================================================================== 225 | 9. License *multiple-cursors-license* 226 | 227 | The project is licensed under the MIT license [7]. Copyright 2013 Terry Ma 228 | 229 | ============================================================================== 230 | 10. Credit *multiple-cursors-credit* 231 | 232 | The plugin is obviously inspired by Sublime Text's awesome multiple selection 233 | [6] feature. Some inspiration was also taken from Emac's multiple cursors [8] 234 | implementation. 235 | 236 | ============================================================================== 237 | 10. References *multiple-cursors-references* 238 | 239 | [1] https://github.com/paradigm/vim-multicursor 240 | [2] https://github.com/felixr/vim-multiedit 241 | [3] https://github.com/hlissner/vim-multiedit 242 | [4] https://github.com/adinapoli/vim-markmultiple 243 | [5] https://github.com/AndrewRadev/multichange.vim 244 | [6] http://www.sublimetext.com/docs/2/multiple_selection_with_the_keyboard.html 245 | [7] http://opensource.org/licenses/MIT 246 | [8] https://github.com/magnars/multiple-cursors.el 247 | [9] http://vim.wikia.com/wiki/Get_Alt_key_to_work_in_terminal 248 | [10] https://code.google.com/p/macvim 249 | 250 | vim:tw=78:sw=4:ft=help:norl: 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **❗ This plugin is deprecated, use [vim-visual-multi](https://github.com/mg979/vim-visual-multi) instead ❗** 2 | 3 | # vim-multiple-cursors 4 | [![Build Status](https://travis-ci.org/terryma/vim-multiple-cursors.svg?branch=master)](https://travis-ci.org/github/terryma/vim-multiple-cursors) 5 | 6 | ## Contents 7 | - [About](#about) 8 | - [Installation](#installation) 9 | - [Quick Start](#quick-start) 10 | - [Mapping](#mapping) 11 | - [Settings](#settings) 12 | - [Interactions with other plugins](#interactions-with-other-plugins) 13 | - [Highlight](#highlight) 14 | - [FAQ](#faq) 15 | - [Contributing](#contributing) 16 | - [Credit](#credit) 17 | 18 | ## About 19 | [There](https://github.com/paradigm/vim-multicursor) [have](https://github.com/felixr/vim-multiedit) [been](https://github.com/hlissner/vim-multiedit) [many](https://github.com/adinapoli/vim-markmultiple) [attempts](https://github.com/AndrewRadev/multichange.vim) at bringing Sublime Text's awesome [multiple selection][sublime-multiple-selection] feature into Vim, but none so far have been in my opinion a faithful port that is simplistic to use, yet powerful and intuitive enough for an existing Vim user. [vim-multiple-cursors] is yet another attempt at that. 20 | 21 | ### It's great for quick refactoring 22 | ![Example1](assets/example1.gif?raw=true) 23 | 24 | Vim command sequence: `fpcname` 25 | 26 | ### Add a cursor to each line of your visual selection 27 | ![Example2](assets/example2.gif?raw=true) 28 | 29 | Vim command sequence: `vipi"",vipgJ$r]Idays = [` 30 | 31 | ### Match characters from visual selection 32 | ![Example3](assets/example3.gif?raw=true) 33 | 34 | Vim command sequence: `df[$r,0f,vc` 35 | 36 | ### Use the command to match regexp 37 | ![Example4](assets/example4.gif?raw=true) 38 | 39 | To see what keystrokes are used for the above examples, see [the wiki page](https://github.com/terryma/vim-multiple-cursors/wiki/Keystrokes-for-example-gifs). 40 | 41 | ## Installation 42 | Install using [Pathogen], [Vundle], [Neobundle], [vim-plug], or your favorite Vim package manager. 43 | 44 | Requires vim 7.4 or newer for full functionality. 45 | 46 | ### vim-plug instructions 47 | 48 | 1. Paste this block into the top of `~/.vimrc`. 49 | 50 | ```vim script 51 | call plug#begin() 52 | 53 | Plug 'terryma/vim-multiple-cursors' 54 | 55 | call plug#end() 56 | ``` 57 | 58 | 2. Start vim and execute `:PlugInstall`. 59 | 60 | ## Quick Start 61 | ### normal mode / visual mode 62 | * start: `` start multicursor and add a _virtual cursor + selection_ on the match 63 | * next: `` add a new _virtual cursor + selection_ on the next match 64 | * skip: `` skip the next match 65 | * prev: `` remove current _virtual cursor + selection_ and go back on previous match 66 | * select all: `` start multicursor and directly select all matches 67 | 68 | You can now change the _virtual cursors + selection_ with **visual mode** commands. 69 | For instance: `c`, `s`, `I`, `A` work without any issues. 70 | You could also go to **normal mode** by pressing `v` and use normal commands there. 71 | 72 | At any time, you can press `` to exit back to regular Vim. 73 | 74 | **NOTE**: start with `g` to match without boundaries (behaves like `g*` instead of `*`) 75 | 76 | ### visual mode when multiple lines are selected 77 | * start: `` add _virtual cursors_ on each line 78 | 79 | You can now change the _virtual cursors_ with **normal mode** commands. 80 | For instance: `ciw`. 81 | 82 | ### command 83 | The command `MultipleCursorsFind` accepts a range and a pattern (regexp), it creates a _visual cursor_ at the end of each match. 84 | If no range is passed in, then it defaults to the entire buffer. 85 | 86 | 87 | ## Mapping 88 | If you don't like the plugin taking over your key bindings, you can turn it off and reassign them the way you want: 89 | ```viml 90 | let g:multi_cursor_use_default_mapping=0 91 | 92 | " Default mapping 93 | let g:multi_cursor_start_word_key = '' 94 | let g:multi_cursor_select_all_word_key = '' 95 | let g:multi_cursor_start_key = 'g' 96 | let g:multi_cursor_select_all_key = 'g' 97 | let g:multi_cursor_next_key = '' 98 | let g:multi_cursor_prev_key = '' 99 | let g:multi_cursor_skip_key = '' 100 | let g:multi_cursor_quit_key = '' 101 | ``` 102 | 103 | **NOTE:** Please make sure to always map something to `g:multi_cursor_quit_key`, otherwise you'll have a tough time quitting from multicursor mode. 104 | 105 | ## Settings 106 | Currently there are four additional global settings one can tweak: 107 | 108 | ### ```g:multi_cursor_support_imap``` (Default: 1) 109 | If set to 0, insert mappings won't be supported in _Insert_ mode anymore. 110 | 111 | ### ```g:multi_cursor_exit_from_visual_mode``` (Default: 0) 112 | If set to 1, then pressing `g:multi_cursor_quit_key` in _Visual_ mode will quit and 113 | delete all existing cursors, just skipping normal mode with multiple cursors. 114 | 115 | ### ```g:multi_cursor_exit_from_insert_mode``` (Default: 0) 116 | If set to 1, then pressing `g:multi_cursor_quit_key` in _Insert_ mode will quit and 117 | delete all existing cursors, just skipping normal mode with multiple cursors. 118 | 119 | ### ```g:multi_cursor_normal_maps``` (Default: see below) 120 | `{'@': 1, 'F': 1, 'T': 1, '[': 1, '\': 1, ']': 1, '!': 1, '"': 1, 'c': 1, 'd': 1, 'f': 1, 'g': 1, 'm': 1, 'q': 1, 'r': 1, 't': 1, 'y': 1, 'z': 1, '<': 1, '=': 1, '>': 1}` 121 | 122 | Any key in this map (values are ignored) will cause multi-cursor _Normal_ mode 123 | to pause for map completion just like normal vim. Otherwise keys mapped in 124 | normal mode will "fail to replay" when multiple cursors are active. 125 | For example: `{'d':1}` makes normal-mode command `dw` work in multi-cursor mode. 126 | 127 | The default list contents should work for anybody, unless they have remapped a 128 | key from an operator-pending command to a non-operator-pending command or 129 | vice versa. 130 | 131 | These keys must be manually listed because vim doesn't provide a way to 132 | automatically see which keys _start_ mappings, and trying to run motion commands 133 | such as `j` as if they were operator-pending commands can break things. 134 | 135 | ### ```g:multi_cursor_visual_maps``` (Default: see below) 136 | `{'T': 1, 'a': 1, 't': 1, 'F': 1, 'f': 1, 'i': 1}` 137 | 138 | Same principle as `g:multi_cursor_normal_maps` 139 | 140 | ### Interactions with other plugins 141 | 142 | ### ```Multiple_cursors_before/Multiple_cursors_after``` (Default: `nothing`) 143 | 144 | Other plugins may be incompatible in insert mode. 145 | That is why we provide hooks to disable those plug-ins when vim-multiple-cursors is active: 146 | 147 | For example, if you are using [Neocomplete](https://github.com/Shougo/neocomplete.vim), 148 | add this to your vimrc to prevent conflict: 149 | 150 | ```viml 151 | function! Multiple_cursors_before() 152 | if exists(':NeoCompleteLock')==2 153 | exe 'NeoCompleteLock' 154 | endif 155 | endfunction 156 | 157 | function! Multiple_cursors_after() 158 | if exists(':NeoCompleteUnlock')==2 159 | exe 'NeoCompleteUnlock' 160 | endif 161 | endfunction 162 | ``` 163 | 164 | Plugins themselves can register `User` autocommands on `MultipleCursorsPre` and 165 | `MultipleCursorsPost` for automatic integration. 166 | 167 | ### Highlight 168 | The plugin uses the highlight group `multiple_cursors_cursor` and `multiple_cursors_visual` to highlight the virtual cursors and their visual selections respectively. You can customize them by putting something similar like the following in your vimrc: 169 | 170 | ```viml 171 | " Default highlighting (see help :highlight and help :highlight-link) 172 | highlight multiple_cursors_cursor term=reverse cterm=reverse gui=reverse 173 | highlight link multiple_cursors_visual Visual 174 | ``` 175 | 176 | ## FAQ 177 | 178 | #### **Q** Pressing i after selecting words with C-n makes the plugin hang, why? 179 | **A** When selecting words with C-n, the plugin behaves like in **visual** mode. 180 | Once you pressed i, you can still press I to insert text. 181 | 182 | #### **Q** ALT+n doesn't seem to work in VIM but works in gVIM, why? 183 | **A** This is a well known terminal/Vim [issue](http://vim.wikia.com/wiki/Get_Alt_key_to_work_in_terminal), different terminal have different ways to send ```Alt+key```. 184 | Try adding this in your `.vimrc` and **make sure to replace the string**: 185 | ```vim 186 | if !has('gui_running') 187 | map "in Insert mode, type Ctrl+v Alt+n here" 188 | endif 189 | ``` 190 | Or remap the following: 191 | ```vim 192 | g:multi_cursor_start_key 193 | g:multi_cursor_select_all_key 194 | ``` 195 | 196 | #### **Q** CTRL+n doesn't seem to work in gVIM? 197 | **A** Try setting `set selection=inclusive` in your `~/.gvimrc` 198 | 199 | **A** Alternatively, you can just temporarily disable _exclusive_ selection whenever the plugin is active: 200 | ```VimL 201 | augroup MultipleCursorsSelectionFix 202 | autocmd User MultipleCursorsPre if &selection ==# 'exclusive' | let g:multi_cursor_save_selection = &selection | set selection=inclusive | endif 203 | autocmd User MultipleCursorsPost if exists('g:multi_cursor_save_selection') | let &selection = g:multi_cursor_save_selection | unlet g:multi_cursor_save_selection | endif 204 | augroup END 205 | ``` 206 | 207 | ### **Q** deoplete insert giberrish, how to fix this? 208 | **A** use the `Multiple_cursors` functions, add this in your vimrc: 209 | 210 | ```VimL 211 | func! Multiple_cursors_before() 212 | if deoplete#is_enabled() 213 | call deoplete#disable() 214 | let g:deoplete_is_enable_before_multi_cursors = 1 215 | else 216 | let g:deoplete_is_enable_before_multi_cursors = 0 217 | endif 218 | endfunc 219 | func! Multiple_cursors_after() 220 | if g:deoplete_is_enable_before_multi_cursors 221 | call deoplete#enable() 222 | endif 223 | endfunc 224 | ``` 225 | 226 | #### **Q** is it also working on Mac? 227 | **A** On Mac OS, [MacVim](https://code.google.com/p/macvim/) is known to work. 228 | 229 | #### **Q** How can I select `n` keywords with several keystrokes? `200` does not work. 230 | **A** You can use :MultipleCursorsFind keyword. I have this binding in my vimrc: 231 | 232 | ```VimL 233 | nnoremap :MultipleCursorsFind / 234 | vnoremap :MultipleCursorsFind / 235 | ``` 236 | 237 | This allows one to search for the keyword using `*` and turn search results into cursors with `Alt-j`. 238 | 239 | 240 | ## Contributing 241 | Patches and suggestions are always welcome! A list of open feature requests can be found [here](https://github.com/terryma/vim-multiple-cursors/labels/pull%20request%20welcome). 242 | 243 | ### Issue Creation 244 | Contributor's time is precious and limited. Please ensure it meets the requirements outlined in [CONTRIBUTING.md](CONTRIBUTING.md). 245 | 246 | ### Pull Requests 247 | Running the test suite requires ruby and rake as well as vim of course. Before submitting PR, please ensure the checks are passing: 248 | ```bash 249 | cd vim-multiple-cursors/spec/ 250 | bundle exec rake 251 | ``` 252 | 253 | ### Contributors 254 | This is a community supported project. Here is the list of all the [Contributors](https://github.com/terryma/vim-multiple-cursors/graphs/contributors) 255 | 256 | ## Credit 257 | Obviously inspired by Sublime Text's [multiple selection][sublime-multiple-selection] feature, also encouraged by Emac's [multiple cursors][emacs-multiple-cursors] implementation by Magnar Sveen 258 | 259 | [vim-multiple-cursors]:http://github.com/terryma/vim-multiple-cursors 260 | [sublime-multiple-selection]:http://www.sublimetext.com/docs/2/multiple_selection_with_the_keyboard.html 261 | [Pathogen]:http://github.com/tpope/vim-pathogen 262 | [Vundle]:http://github.com/gmarik/vundle 263 | [Neobundle]:http://github.com/Shougo/neobundle.vim 264 | [vim-plug]:https://github.com/junegunn/vim-plug 265 | [emacs-multiple-cursors]:https://github.com/magnars/multiple-cursors.el 266 | -------------------------------------------------------------------------------- /spec/multiple_cursors_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'spec_helper' 3 | 4 | def set_file_content(string) 5 | string = normalize_string_indent(string) 6 | File.open(filename, 'w'){ |f| f.write(string) } 7 | vim.edit filename 8 | end 9 | 10 | def get_file_content() 11 | vim.write 12 | IO.read(filename).strip 13 | end 14 | 15 | def before(string) 16 | options.each { |x| vim.command(x) } 17 | set_file_content(string) 18 | end 19 | 20 | def after(string) 21 | expect(get_file_content()).to eq normalize_string_indent(string) 22 | end 23 | 24 | def type(string) 25 | string.scan(/<.*?>|./).each do |key| 26 | if /<.*>/.match(key) 27 | vim.feedkeys "\\#{key}" 28 | else 29 | vim.feedkeys key 30 | end 31 | end 32 | end 33 | 34 | describe "Multiple Cursors op pending & exit from insert|visual mode" do 35 | let(:filename) { 'test.txt' } 36 | let(:options) { ['let g:multi_cursor_exit_from_insert_mode = 0', 37 | 'let g:multi_cursor_exit_from_visual_mode = 0'] } 38 | # the default value of g:multi_cursor_normal_maps already works 39 | # for testing operator-pending 40 | 41 | specify "#paste from unnamed register to 3 cursors" do 42 | before <<-EOF 43 | yankme 44 | a b c 45 | a b c 46 | a b c 47 | EOF 48 | 49 | type 'yiwjvwwp' 50 | 51 | after <<-EOF 52 | yankme 53 | a b cyankme 54 | a b cyankme 55 | a b cyankme 56 | EOF 57 | end 58 | 59 | specify "#paste buffer normal caw then p" do 60 | before <<-EOF 61 | hello jan world 62 | hello feb world 63 | hello mar world 64 | EOF 65 | 66 | type 'vwcawbP' 67 | 68 | after <<-EOF 69 | jan hello world 70 | feb hello world 71 | mar hello world 72 | EOF 73 | end 74 | 75 | specify "#paste buffer normal C then ABC then p" do 76 | before <<-EOF 77 | hello jan world 78 | hello feb world 79 | hello mar world 80 | EOF 81 | 82 | type 'vwCABC p' 83 | 84 | after <<-EOF 85 | hello ABC jan world 86 | hello ABC feb world 87 | hello ABC mar world 88 | EOF 89 | end 90 | 91 | specify "#paste buffer normal daw then P" do 92 | before <<-EOF 93 | hello jan world 94 | hello feb world 95 | hello mar world 96 | EOF 97 | 98 | type 'vwdawbP' 99 | 100 | after <<-EOF 101 | jan hello world 102 | feb hello world 103 | mar hello world 104 | EOF 105 | end 106 | 107 | specify "#paste buffer normal D then P" do 108 | before <<-EOF 109 | hello jan world 110 | hello feb world 111 | hello mar world 112 | EOF 113 | 114 | type 'vwwhDbhP' 115 | 116 | after <<-EOF 117 | hello world jan 118 | hello world feb 119 | hello world mar 120 | EOF 121 | end 122 | 123 | specify "#paste buffer normal s then p" do 124 | before <<-EOF 125 | hello jan world 126 | hello feb world 127 | hello mar world 128 | EOF 129 | 130 | type 'vws1p' 131 | 132 | after <<-EOF 133 | hello 1jan world 134 | hello 1feb world 135 | hello 1mar world 136 | EOF 137 | end 138 | 139 | specify "#normal mode '0': goes to 1st char of line" do 140 | before <<-EOF 141 | hello jan world 142 | hello feb world 143 | hello mar world 144 | EOF 145 | 146 | type 'vw0dw' 147 | 148 | after <<-EOF 149 | jan world 150 | feb world 151 | mar world 152 | EOF 153 | end 154 | 155 | specify "#normal mode 'd0': deletes backward to 1st char of line" do 156 | before <<-EOF 157 | hello jan world 158 | hello feb world 159 | hello mar world 160 | EOF 161 | 162 | type 'vwd0' 163 | 164 | after <<-EOF 165 | jan world 166 | feb world 167 | mar world 168 | EOF 169 | end 170 | 171 | end 172 | 173 | describe "Multiple Cursors when using insert mappings" do 174 | let(:filename) { 'test.txt' } 175 | let(:options) { ['set timeoutlen=10000', 176 | 'imap jj ', 177 | 'imap jojo dude', 178 | 'imap jk :%s/bla/hey/g', 179 | 'let g:multi_cursor_exit_from_insert_mode = 1', 180 | 'let g:multi_cursor_exit_from_visual_mode = 1'] } 181 | specify "#mapping doing " do 182 | before <<-EOF 183 | hello world! 184 | hello world! 185 | bla bla bla 186 | bla bla bla 187 | EOF 188 | 189 | type 'wcjjidude' 190 | 191 | after <<-EOF 192 | hello dude! 193 | hello ! 194 | bla bla bla 195 | bla bla bla 196 | EOF 197 | end 198 | 199 | specify "#mapping doing and running a command" do 200 | before <<-EOF 201 | hello world! 202 | hello world! 203 | bla bla bla 204 | bla bla bla 205 | EOF 206 | 207 | type 'wctherejk' 208 | 209 | after <<-EOF 210 | hello there! 211 | hello there! 212 | hey hey hey 213 | hey hey hey 214 | EOF 215 | end 216 | 217 | specify "#mapping using more than 2 characters" do 218 | before <<-EOF 219 | hello 220 | hello 221 | bla bla bla 222 | bla bla bla 223 | EOF 224 | 225 | type 'A jojo' 226 | 227 | after <<-EOF 228 | hello dude 229 | hello dude 230 | bla bla bla 231 | bla bla bla 232 | EOF 233 | end 234 | 235 | specify "#unused mapping" do 236 | before <<-EOF 237 | hello world! 238 | hello world! 239 | bla bla bla 240 | bla bla bla 241 | EOF 242 | 243 | type 'wchey joseph blah blah blah' 244 | 245 | after <<-EOF 246 | hello hey joseph blah blah blah! 247 | hello hey joseph blah blah blah! 248 | bla bla bla 249 | bla bla bla 250 | EOF 251 | end 252 | 253 | end 254 | 255 | describe "Multiple Cursors when normal_maps is empty" do 256 | let(:filename) { 'test.txt' } 257 | let(:options) { ['let g:multi_cursor_normal_maps = {}'] } 258 | 259 | # Operator-pending commands are handled correctly thanks to their inclusion 260 | # in `g:multi_cursor_normal_maps`. 261 | # 262 | # When an operator-pending command like 'd' is missing from that setting's 263 | # value, then it should result in a no-op, but we should still remain in 264 | # multicursor mode. 265 | specify "#normal mode 'd'" do 266 | before <<-EOF 267 | hello 268 | hello 269 | EOF 270 | 271 | type 'vdx' 272 | 273 | after <<-EOF 274 | hell 275 | hell 276 | EOF 277 | end 278 | 279 | end 280 | 281 | describe "Multiple Cursors when visual_maps is empty" do 282 | let(:filename) { 'test.txt' } 283 | let(:options) { ['let g:multi_cursor_visual_maps = {}'] } 284 | 285 | # Operator-pending commands are handled correctly thanks to their inclusion 286 | # in `g:multi_cursor_visual_maps`. 287 | # 288 | # When an operator-pending command like 'f' is missing from that setting's 289 | # value, then it should result in a no-op, but we should still remain in 290 | # multicursor mode. 291 | specify "#visual mode 'i'" do 292 | before <<-EOF 293 | hello world x 294 | hello world x 295 | EOF 296 | 297 | type 'fwfx' 298 | 299 | after <<-EOF 300 | hello x 301 | hello x 302 | EOF 303 | end 304 | 305 | end 306 | 307 | describe "Multiple Cursors when changing the line count" do 308 | let(:filename) { 'test.txt' } 309 | let(:options) { ['set backspace=indent,eol,start'] } 310 | 311 | specify "#backspace on first char of the line, then carriage return" do 312 | before <<-EOF 313 | madec 314 | 315 | antoine 316 | andre 317 | joseph 318 | EOF 319 | 320 | type 'Gvipi' 321 | 322 | after <<-EOF 323 | madec 324 | 325 | antoine 326 | andre 327 | joseph 328 | EOF 329 | end 330 | 331 | specify "#del at EOL, then carriage return" do 332 | before <<-EOF 333 | madec 334 | antoine 335 | joseph 336 | 337 | andre 338 | EOF 339 | 340 | type 'vipA' 341 | 342 | after <<-EOF 343 | madec 344 | antoine 345 | joseph 346 | 347 | andre 348 | EOF 349 | end 350 | 351 | end 352 | 353 | describe "Multiple Cursors misc" do 354 | let(:filename) { 'test.txt' } 355 | let(:options) { ['set autoindent'] } 356 | 357 | specify "#paste buffer normal x then p" do 358 | before <<-EOF 359 | jan 360 | feb 361 | mar 362 | EOF 363 | 364 | type 'jjxp' 365 | 366 | after <<-EOF 367 | ajn 368 | efb 369 | amr 370 | EOF 371 | end 372 | 373 | specify "#paste buffer visual y then p" do 374 | before <<-EOF 375 | hello jan world 376 | hello feb world 377 | hello mar world 378 | EOF 379 | 380 | type 'vwvelywhp' 381 | 382 | after <<-EOF 383 | hello jan jan world 384 | hello feb feb world 385 | hello mar mar world 386 | EOF 387 | end 388 | 389 | specify "#paste buffer initial visual y then P" do 390 | before <<-EOF 391 | hello jan world 392 | hello feb world 393 | hello mar world 394 | EOF 395 | 396 | type 'wywbp' 397 | 398 | after <<-EOF 399 | jan jan world 400 | jan feb world 401 | jan mar world 402 | EOF 403 | end 404 | 405 | specify "#paste buffer visual y then P" do 406 | before <<-EOF 407 | hello jan world 408 | hello feb world 409 | hello mar world 410 | EOF 411 | 412 | type 'vwvely^P' 413 | 414 | after <<-EOF 415 | jan hello jan world 416 | feb hello feb world 417 | mar hello mar world 418 | EOF 419 | end 420 | 421 | specify "#paste buffer visual Y then P" do 422 | before <<-EOF 423 | hello jan world 424 | hello feb world 425 | hello mar world 426 | EOF 427 | 428 | type 'vwvY^P' 429 | 430 | after <<-EOF 431 | hello jan world 432 | hello jan world 433 | hello feb world 434 | hello feb world 435 | hello mar world 436 | hello mar world 437 | EOF 438 | end 439 | 440 | specify "#multiline replacement" do 441 | before <<-EOF 442 | hello 443 | hello 444 | hello 445 | EOF 446 | 447 | type 'cworld' 448 | 449 | after <<-EOF 450 | world 451 | world 452 | world 453 | EOF 454 | end 455 | 456 | specify "#single line replacement" do 457 | before <<-EOF 458 | hello hello hello 459 | EOF 460 | 461 | type 'cworld' 462 | 463 | after <<-EOF 464 | world world world 465 | EOF 466 | end 467 | 468 | specify "#mixed line replacement" do 469 | before <<-EOF 470 | hello hello 471 | hello 472 | EOF 473 | 474 | type 'cworld' 475 | 476 | after <<-EOF 477 | world world 478 | world 479 | EOF 480 | end 481 | 482 | specify "#new line in insert mode" do 483 | before <<-EOF 484 | hello 485 | hello 486 | EOF 487 | 488 | type 'chelloworld' 489 | 490 | after <<-EOF 491 | hello 492 | world 493 | hello 494 | world 495 | EOF 496 | end 497 | 498 | specify "#new line in insert mode middle of line" do 499 | before <<-EOF 500 | hello world 501 | hello world 502 | EOF 503 | 504 | type 'vlxi' 505 | 506 | after <<-EOF 507 | hello 508 | world 509 | hello 510 | world 511 | EOF 512 | end 513 | 514 | specify "#multiple new lines on one line in insert mode" do 515 | before <<-EOF 516 | 'a','b','c','d','e' 517 | EOF 518 | 519 | type 'f,vc' 520 | 521 | after <<-EOF 522 | 'a' 523 | 'b' 524 | 'c' 525 | 'd' 526 | 'e' 527 | EOF 528 | end 529 | 530 | specify "#multiple new lines on one line in insert mode with indents" do 531 | before <<-EOF 532 | 'a','b','c','d','e' 533 | EOF 534 | 535 | type '4if,vc:%s/^/^' 536 | 537 | after <<-EOF 538 | ^ 'a' 539 | ^ 'b' 540 | ^ 'c' 541 | ^ 'd' 542 | ^ 'e' 543 | EOF 544 | end 545 | 546 | specify "#normal mode 'o'" do 547 | before <<-EOF 548 | hello 549 | hello 550 | EOF 551 | 552 | type 'voworld' 553 | 554 | after <<-EOF 555 | hello 556 | world 557 | hello 558 | world 559 | EOF 560 | end 561 | 562 | specify "#normal mode 'O'" do 563 | before <<-EOF 564 | hello 565 | hello 566 | EOF 567 | 568 | type 'vOworld' 569 | 570 | after <<-EOF 571 | world 572 | hello 573 | world 574 | hello 575 | EOF 576 | end 577 | 578 | specify "#find command basic" do 579 | before <<-EOF 580 | hello 581 | hello 582 | EOF 583 | 584 | vim.normal ':MultipleCursorsFind hello' 585 | type 'cworld' 586 | 587 | after <<-EOF 588 | world 589 | world 590 | EOF 591 | end 592 | 593 | specify "#find command start-of-line" do 594 | before <<-EOF 595 | hello 596 | world 597 | 598 | hello 599 | world 600 | EOF 601 | 602 | vim.normal ':MultipleCursorsFind ^' 603 | type 'Ibegin' 604 | 605 | after <<-EOF 606 | beginhello 607 | beginworld 608 | begin 609 | beginhello 610 | beginworld 611 | EOF 612 | end 613 | 614 | specify "#find command end-of-line" do 615 | before <<-EOF 616 | hello 617 | world 618 | 619 | hello 620 | world 621 | EOF 622 | 623 | vim.normal ':MultipleCursorsFind $' 624 | type 'Iend' 625 | 626 | after <<-EOF 627 | helloend 628 | worldend 629 | end 630 | helloend 631 | worldend 632 | EOF 633 | end 634 | 635 | specify "#visual line mode replacement" do 636 | before <<-EOF 637 | hello world 638 | hello world 639 | EOF 640 | 641 | type 'Vchi!' 642 | 643 | after <<-EOF 644 | hi! 645 | hi! 646 | EOF 647 | end 648 | 649 | specify "#skip key" do 650 | before <<-EOF 651 | hello 652 | hello 653 | hello 654 | EOF 655 | 656 | type 'cworld' 657 | 658 | after <<-EOF 659 | world 660 | hello 661 | world 662 | EOF 663 | end 664 | 665 | specify "#prev key" do 666 | before <<-EOF 667 | hello 668 | hello 669 | hello 670 | EOF 671 | 672 | type 'cworld' 673 | 674 | after <<-EOF 675 | world 676 | world 677 | hello 678 | EOF 679 | end 680 | 681 | specify "#visual mode 'i'" do 682 | before <<-EOF 683 | hi (hello world jan) bye 684 | hi (hello world feb) bye 685 | hi (hello world mar) bye 686 | EOF 687 | 688 | type 'fwibcone' 689 | 690 | after <<-EOF 691 | hi (one) bye 692 | hi (one) bye 693 | hi (one) bye 694 | EOF 695 | end 696 | 697 | specify "#visual mode 'a'" do 698 | before <<-EOF 699 | hi (hello world jan) bye 700 | hi (hello world feb) bye 701 | hi (hello world mar) bye 702 | EOF 703 | 704 | type 'fwabcone' 705 | 706 | after <<-EOF 707 | hi one bye 708 | hi one bye 709 | hi one bye 710 | EOF 711 | end 712 | 713 | specify "#visual mode 'f'" do 714 | before <<-EOF 715 | hi (hello world jan) bye 716 | hi (hello world feb) bye 717 | hi (hello world mar) bye 718 | EOF 719 | 720 | type 'fwf)cone' 721 | 722 | after <<-EOF 723 | hi (hello one bye 724 | hi (hello one bye 725 | hi (hello one bye 726 | EOF 727 | end 728 | 729 | specify "#visual mode 'F'" do 730 | before <<-EOF 731 | hi (hello world jan) bye 732 | hi (hello world feb) bye 733 | hi (hello world mar) bye 734 | EOF 735 | 736 | type 'fwF(cbefore' 737 | 738 | after <<-EOF 739 | hi beforeorld jan) bye 740 | hi beforeorld feb) bye 741 | hi beforeorld mar) bye 742 | EOF 743 | end 744 | 745 | specify "#visual mode 't'" do 746 | before <<-EOF 747 | hello.jan 748 | hello hi.feb 749 | hello hi bye.mar 750 | EOF 751 | 752 | type 't.cone' 753 | 754 | after <<-EOF 755 | one.jan 756 | one.feb 757 | one.mar 758 | EOF 759 | end 760 | 761 | specify "#visual mode 'T'" do 762 | before <<-EOF 763 | jan.world 764 | feb.hi world 765 | mar.bye hi world 766 | EOF 767 | 768 | type 'fwT.cbefore' 769 | 770 | after <<-EOF 771 | jan.beforeorld 772 | feb.beforeorld 773 | mar.beforeorld 774 | EOF 775 | end 776 | 777 | specify "#visual line mode 'f'" do 778 | before <<-EOF 779 | hello jan world 780 | hello feb world 781 | hello mar world 782 | EOF 783 | 784 | type 'VfwvAafter' 785 | 786 | after <<-EOF 787 | hello jan wafterorld 788 | hello feb wafterorld 789 | hello mar wafterorld 790 | EOF 791 | end 792 | 793 | specify "#visual mode 'I'" do 794 | before <<-EOF 795 | hello world jan 796 | hello world feb 797 | hello world mar 798 | EOF 799 | 800 | type 'wIbefore' 801 | 802 | after <<-EOF 803 | hello beforeworld jan 804 | hello beforeworld feb 805 | hello beforeworld mar 806 | EOF 807 | end 808 | 809 | specify "#visual mode 'A'" do 810 | before <<-EOF 811 | hello world jan 812 | hello world feb 813 | hello world mar 814 | EOF 815 | 816 | type 'wAafter' 817 | 818 | after <<-EOF 819 | hello worldafter jan 820 | hello worldafter feb 821 | hello worldafter mar 822 | EOF 823 | end 824 | 825 | specify "#resize regions visual mode 'I'" do 826 | before <<-EOF 827 | hello world jan 828 | hello world feb 829 | hello world mar 830 | EOF 831 | 832 | type 'whhhIbefore' 833 | 834 | after <<-EOF 835 | hello beforeworld jan 836 | hello beforeworld feb 837 | hello beforeworld mar 838 | EOF 839 | end 840 | 841 | specify "#resize regions visual mode 'A'" do 842 | before <<-EOF 843 | hello world jan 844 | hello world feb 845 | hello world mar 846 | EOF 847 | 848 | type 'whhhAbefore' 849 | 850 | after <<-EOF 851 | hello wobeforerld jan 852 | hello wobeforerld feb 853 | hello wobeforerld mar 854 | EOF 855 | end 856 | 857 | specify "#no word boundries visual mode 'I'" do 858 | before <<-EOF 859 | hello hibye world 860 | hello hibye world 861 | hello hibye world 862 | EOF 863 | 864 | vim.normal ':MultipleCursorsFind bye' 865 | type 'Ibefore' 866 | 867 | after <<-EOF 868 | hello hibeforebye world 869 | hello hibeforebye world 870 | hello hibeforebye world 871 | EOF 872 | end 873 | 874 | specify "#variable-length regions visual mode 'I'" do 875 | before <<-EOF 876 | hello hii world 877 | hello hiiii world 878 | hello hiiiiii world 879 | EOF 880 | 881 | vim.normal ':MultipleCursorsFind \' 882 | type 'Ibefore' 883 | 884 | after <<-EOF 885 | hello beforehii world 886 | hello beforehiiii world 887 | hello beforehiiiiii world 888 | EOF 889 | end 890 | 891 | specify "#normal mode 'I'" do 892 | before <<-EOF 893 | hello 894 | hello 895 | EOF 896 | 897 | type 'vIworld ' 898 | 899 | after <<-EOF 900 | world hello 901 | world hello 902 | EOF 903 | end 904 | 905 | specify "#normal mode 'A'" do 906 | before <<-EOF 907 | hello 908 | hello 909 | EOF 910 | 911 | type 'vA world' 912 | 913 | after <<-EOF 914 | hello world 915 | hello world 916 | EOF 917 | end 918 | 919 | specify "#undo" do 920 | before <<-EOF 921 | hello 922 | hello 923 | EOF 924 | 925 | type 'cworldu' 926 | 927 | after <<-EOF 928 | hello 929 | hello 930 | EOF 931 | end 932 | 933 | specify "#multiline visual mode" do 934 | before <<-EOF 935 | hello 936 | hello 937 | EOF 938 | 939 | type 'VjA world' 940 | 941 | after <<-EOF 942 | hello world 943 | hello world 944 | EOF 945 | end 946 | 947 | specify "#set paste mode" do 948 | before <<-EOF 949 | hello 950 | hello 951 | EOF 952 | 953 | type ':set pastecworld:set nopaste' 954 | 955 | after <<-EOF 956 | world 957 | world 958 | EOF 959 | end 960 | 961 | specify "#multi-byte strings" do 962 | before <<-EOF 963 | こんにちわビム 964 | 世界の中心でビムを叫ぶ 965 | ビム大好き 966 | EOF 967 | 968 | type '/ビムcヴィム' 969 | 970 | after <<-EOF 971 | こんにちわヴィム 972 | 世界の中心でヴィムを叫ぶ 973 | ヴィム大好き 974 | EOF 975 | end 976 | 977 | end 978 | -------------------------------------------------------------------------------- /autoload/multiple_cursors.vim: -------------------------------------------------------------------------------- 1 | "=============================================================================== 2 | " Initialization 3 | "=============================================================================== 4 | 5 | " Tweak key settings. If the key is set using 'expr-quote' (h: expr-quote), then 6 | " there's nothing that we need to do. If it's set using raw strings, then we 7 | " need to convert it. We need to resort to such voodoo exec magic here to get 8 | " it to work the way we like. '' is converted to '\' by the end and 9 | " the global vars are replaced by their new value. This is ok since the mapping 10 | " using '' should already have completed in the plugin file. 11 | for s:key in [ 'g:multi_cursor_next_key', 12 | \ 'g:multi_cursor_prev_key', 13 | \ 'g:multi_cursor_skip_key', 14 | \ 'g:multi_cursor_quit_key' ] 15 | if exists(s:key) 16 | " Translate raw strings like "" into key code like "\" 17 | exec 'let s:temp = '.s:key 18 | if s:temp =~ '^<.*>$' 19 | exec 'let '.s:key.' = "\'.s:temp.'"' 20 | endif 21 | else 22 | " If the user didn't define it, initialize it to an empty string so the 23 | " logic later don't break 24 | exec 'let '.s:key.' = ""' 25 | endif 26 | endfor 27 | unlet! s:key s:temp 28 | 29 | " These keys will not be replicated at every cursor location. Make sure that 30 | " this assignment happens AFTER the key tweak setting above 31 | let s:special_keys = { 32 | \ 'v': [ g:multi_cursor_next_key, g:multi_cursor_prev_key, g:multi_cursor_skip_key ], 33 | \ 'n': [ g:multi_cursor_next_key ], 34 | \ } 35 | 36 | " The highlight group we use for all the cursors 37 | let s:hi_group_cursor = 'multiple_cursors_cursor' 38 | 39 | " The highlight group we use for all the visual selection 40 | let s:hi_group_visual = 'multiple_cursors_visual' 41 | 42 | " Used for preventing multiple calls on before function 43 | let s:before_function_called = 0 44 | 45 | " Used for searching whole words (search pattern is wrapped with \< and \>) 46 | " Keep old behaviour by default (act like g*) 47 | let s:use_word_boundary = 1 48 | 49 | " Set up highlighting 50 | if !hlexists(s:hi_group_cursor) 51 | exec "highlight ".s:hi_group_cursor." term=reverse cterm=reverse gui=reverse" 52 | endif 53 | if !hlexists(s:hi_group_visual) 54 | exec "highlight link ".s:hi_group_visual." Visual" 55 | endif 56 | 57 | " Temporary buffer that is used for individual paste buffer save/restore 58 | " operations 59 | let s:paste_buffer_temporary_text = '' 60 | let s:paste_buffer_temporary_type = '' 61 | 62 | "=============================================================================== 63 | " Internal Mappings 64 | "=============================================================================== 65 | 66 | inoremap (multiple-cursors-input) :call process_user_input() 67 | nnoremap (multiple-cursors-input) :call process_user_input() 68 | xnoremap (multiple-cursors-input) :call process_user_input() 69 | 70 | inoremap (multiple-cursors-apply) :call apply_user_input_next('i') 71 | nnoremap (multiple-cursors-apply) :call apply_user_input_next('n') 72 | xnoremap (multiple-cursors-apply) :call apply_user_input_next('v') 73 | 74 | inoremap (multiple-cursors-detect) :call detect_bad_input() 75 | nnoremap (multiple-cursors-detect) :call detect_bad_input() 76 | xnoremap (multiple-cursors-detect) :call detect_bad_input() 77 | 78 | inoremap (multiple-cursors-wait) :call wait_for_user_input('') 79 | nnoremap (multiple-cursors-wait) :call wait_for_user_input('') 80 | xnoremap (multiple-cursors-wait) :call wait_for_user_input('') 81 | 82 | " Note that although these mappings are seemingly triggerd from Visual mode, 83 | " they are in fact triggered from Normal mode. We quit visual mode to allow the 84 | " virtual highlighting to take over 85 | nnoremap (multiple-cursors-prev) :call multiple_cursors#prev() 86 | nnoremap (multiple-cursors-skip) :call multiple_cursors#skip() 87 | nnoremap (multiple-cursors-new) :call multiple_cursors#new('v', 0) 88 | nnoremap (multiple-cursors-new-word) :call multiple_cursors#new('v', 1) 89 | 90 | "=============================================================================== 91 | " Public Functions 92 | "=============================================================================== 93 | 94 | " Print some debugging info 95 | function! multiple_cursors#debug() 96 | call s:cm.debug() 97 | endfunction 98 | 99 | function! multiple_cursors#get_latency_debug_file() 100 | return s:latency_debug_file 101 | endfunction 102 | 103 | 104 | function! s:fire_pre_triggers() 105 | if !s:before_function_called 106 | silent doautocmd User MultipleCursorsPre 107 | if exists('*Multiple_cursors_before') 108 | exe "call Multiple_cursors_before()" 109 | endif 110 | let s:before_function_called = 1 111 | endif 112 | endfunction 113 | 114 | " Creates a new cursor. Different logic applies depending on the mode the user 115 | " is in and the current state of the buffer. 116 | " 1. In normal mode, a new cursor is created at the end of the word under Vim's 117 | " normal cursor 118 | " 2. In visual mode, if the visual selection covers more than one line, a new 119 | " cursor is created at the beginning of each line 120 | " 3. In visual mode, if the visual selection covers a single line, a new cursor 121 | " is created at the end of the visual selection. Another cursor will be 122 | " attempted to be created at the next occurrence of the visual selection 123 | function! multiple_cursors#new(mode, word_boundary) 124 | " Call before function if exists only once until it is canceled () 125 | call s:fire_pre_triggers() 126 | let s:use_word_boundary = a:word_boundary 127 | if a:mode ==# 'n' 128 | " Reset all existing cursors, don't restore view and setting 129 | call s:cm.reset(0, 0) 130 | 131 | " Select the word under cursor to set the '< and '> marks 132 | exec "normal! viw" 133 | call s:exit_visual_mode() 134 | 135 | " Add cursor with the current visual selection 136 | call s:cm.add(s:pos("'>"), s:region("'<", "'>")) 137 | call s:wait_for_user_input('v') 138 | elseif a:mode ==# 'v' 139 | " If the visual area covers the same line, then do a search for next 140 | " occurrence 141 | let start = line("'<") 142 | let finish = line("'>") 143 | if start != finish 144 | call s:cm.reset(0, 0) 145 | let col = col("'<") 146 | for line in range(line("'<"), line("'>")) 147 | let pos = [line, col] 148 | call s:cm.add(pos) 149 | endfor 150 | " Start in normal mode 151 | call s:wait_for_user_input('n') 152 | else 153 | " Came directly from visual mode 154 | if s:cm.is_empty() 155 | call s:cm.reset(0, 0) 156 | 157 | if visualmode() ==# 'V' 158 | let left = [line('.'), 1] 159 | let right = [line('.'), col('$')-1] 160 | if right[1] == 0 " empty line 161 | return 162 | endif 163 | call s:cm.add(right, [left, right]) 164 | else 165 | call s:cm.add(s:pos("'>"), s:region("'<", "'>")) 166 | endif 167 | endif 168 | let content = s:get_text(s:region("'<", "'>")) 169 | let next = s:find_next(content) 170 | if s:cm.add(next[1], next) 171 | call s:update_visual_markers(next) 172 | else 173 | call cursor(s:cm.get_current().position) 174 | echohl WarningMsg | echo 'No more matches' | echohl None 175 | endif 176 | call s:wait_for_user_input('v') 177 | endif 178 | endif 179 | endfunction 180 | 181 | " Quit out of multicursor mode, fixes #27. 182 | function! multiple_cursors#quit() 183 | call s:exit() 184 | endfunction 185 | 186 | " Delete the current cursor. If there's no more cursors, stop the loop 187 | function! multiple_cursors#prev() 188 | call s:cm.delete_current() 189 | if !s:cm.is_empty() 190 | call s:update_visual_markers(s:cm.get_current().visual) 191 | call cursor(s:cm.get_current().position) 192 | call s:wait_for_user_input('v') 193 | endif 194 | endfunction 195 | 196 | " Skip the current cursor and move to the next cursor 197 | function! multiple_cursors#skip() 198 | call s:cm.delete_current() 199 | let content = s:get_text(s:region("'<", "'>")) 200 | let next = s:find_next(content) 201 | call s:cm.add(next[1], next) 202 | call s:update_visual_markers(next) 203 | call s:wait_for_user_input('v') 204 | endfunction 205 | 206 | " Search for pattern between the start and end line number. For each match, add 207 | " a virtual cursor at the end and start multicursor mode 208 | " This function is called from a command. User commands in Vim do not support 209 | " passing in column ranges. If the user selects a block of text in visual mode, 210 | " but not visual line mode, we only want to match patterns within the actual 211 | " visual selection. We get around this by checking the last visual selection and 212 | " see if its start and end lines match the input. If so, we assume that the user 213 | " did a normal visual selection and we use the '< and '> marks to define the 214 | " region instead of start and end from the method parameter. 215 | function! multiple_cursors#find(start, end, pattern) 216 | let s:cm.saved_winview = winsaveview() 217 | let s:cm.start_from_find = 1 218 | if visualmode() ==# 'v' && a:start == line("'<") && a:end == line("'>") 219 | let pos1 = s:pos("'<") 220 | let pos2 = s:pos("'>") 221 | else 222 | let pos1 = [a:start, 1] 223 | let pos2 = [a:end, col([a:end, '$'])] 224 | endif 225 | call cursor(pos1) 226 | let first = 1 227 | while 1 228 | if first 229 | " Set `virtualedit` to 'onemore' for the first search to consistently 230 | " match patterns like '$' 231 | let saved_virtualedit = &virtualedit 232 | let &virtualedit = "onemore" 233 | " First search starts from the current position 234 | let match = search(a:pattern, 'cW') 235 | else 236 | let match = search(a:pattern, 'W') 237 | endif 238 | if !match 239 | break 240 | endif 241 | let left = s:pos('.') 242 | " Perform an intermediate backward search to correctly match patterns like 243 | " '^' and '$' 244 | let match = search(a:pattern, 'bceW') 245 | let right = s:pos('.') 246 | " Reset the cursor and perform a normal search if the intermediate search 247 | " wasn't successful 248 | if !match || s:compare_pos(right, left) != 0 249 | call cursor(left) 250 | call search(a:pattern, 'ceW') 251 | let right = s:pos('.') 252 | endif 253 | if first 254 | let &virtualedit = saved_virtualedit 255 | let first = 0 256 | endif 257 | if s:compare_pos(right, pos2) > 0 258 | " Position the cursor at the end of the previous match so it'll be on a 259 | " virtual cursor when multicursor mode is started. The `winrestview()` 260 | " call below 'undoes' unnecessary repositionings 261 | call search(a:pattern, 'be') 262 | break 263 | endif 264 | call s:cm.add(right, [left, right]) 265 | " Redraw here forces the cursor movement to be updated. This prevents the 266 | " jerky behavior when doing any action once the cursors are added. But it 267 | " also slows down adding the cursors dramatically. We need to a better 268 | " solution here 269 | " redraw 270 | endwhile 271 | if s:cm.is_empty() 272 | call winrestview(s:cm.saved_winview) 273 | echohl ErrorMsg | echo 'No match found' | echohl None 274 | return 275 | else 276 | echohl Normal | echo 'Added '.s:cm.size().' cursor'.(s:cm.size()>1?'s':'') | echohl None 277 | 278 | " If we've created any cursors, we need to call the before function, end 279 | " function will be called via normal routes 280 | call s:fire_pre_triggers() 281 | 282 | call s:wait_for_user_input('v') 283 | endif 284 | endfunction 285 | 286 | " apply multiple_cursors#find() on the whole buffer 287 | function! multiple_cursors#select_all(mode, word_boundary) 288 | if a:mode == 'v' 289 | let a_save = @a 290 | normal! gv"ay 291 | let pattern = @a 292 | let @a = a_save 293 | elseif a:mode == 'n' 294 | let pattern = expand('') 295 | endif 296 | if a:word_boundary == 1 297 | let pattern = '\<'.pattern.'\>' 298 | endif 299 | call multiple_cursors#find(1, line('$'), pattern) 300 | endfunction 301 | 302 | "=============================================================================== 303 | " Cursor class 304 | "=============================================================================== 305 | let s:Cursor = {} 306 | 307 | " Create a new cursor. Highlight it and save the current line length 308 | function! s:Cursor.new(position) 309 | let obj = copy(self) 310 | let obj.position = copy(a:position) 311 | let obj.visual = [] 312 | let obj.saved_visual = [] 313 | " Stores text that was yanked after any commands in Normal or Visual mode 314 | let obj.paste_buffer_text = getreg('"') 315 | let obj.paste_buffer_type = getregtype('"') 316 | let obj.cursor_hi_id = s:highlight_cursor(a:position) 317 | let obj.visual_hi_id = 0 318 | let obj.line_length = col([a:position[0], '$']) 319 | if has('folding') 320 | silent! execute a:position[0] . "foldopen!" 321 | endif 322 | return obj 323 | endfunction 324 | 325 | " Return the line the cursor is on 326 | function! s:Cursor.line() dict 327 | return self.position[0] 328 | endfunction 329 | 330 | " Return the column the cursor is on 331 | function! s:Cursor.column() dict 332 | return self.position[1] 333 | endfunction 334 | 335 | " Move the cursor location by the number of lines and columns specified in the 336 | " input. The input can be negative. 337 | function! s:Cursor.move(line, column) dict 338 | let self.position[0] += a:line 339 | let self.position[1] += a:column 340 | if !empty(self.visual) 341 | let self.visual[0][0] += a:line 342 | let self.visual[0][1] += a:column 343 | let self.visual[1][0] += a:line 344 | let self.visual[1][1] += a:column 345 | endif 346 | call self.update_highlight() 347 | endfunction 348 | 349 | " Update the current position of the cursor 350 | function! s:Cursor.update_position(pos) dict 351 | let self.position[0] = a:pos[0] 352 | let self.position[1] = a:pos[1] 353 | call self.update_highlight() 354 | endfunction 355 | 356 | " Reapply the highlight on the cursor 357 | function! s:Cursor.update_highlight() dict 358 | call s:cm.remove_highlight(self.cursor_hi_id) 359 | let self.cursor_hi_id = s:highlight_cursor(self.position) 360 | endfunction 361 | 362 | " Refresh the length of the line the cursor is on. This could change from 363 | " underneath 364 | function! s:Cursor.update_line_length() dict 365 | let self.line_length = col([self.line(), '$']) 366 | endfunction 367 | 368 | " Update the visual selection and its highlight 369 | function! s:Cursor.update_visual_selection(region) dict 370 | let self.visual = deepcopy(a:region) 371 | call s:cm.remove_highlight(self.visual_hi_id) 372 | let self.visual_hi_id = s:highlight_region(a:region) 373 | endfunction 374 | 375 | " Remove the visual selection and its highlight 376 | function! s:Cursor.remove_visual_selection() dict 377 | let self.saved_visual = deepcopy(self.visual) 378 | let self.visual = [] 379 | " TODO(terryma): Move functionality into separate class 380 | call s:cm.remove_highlight(self.visual_hi_id) 381 | let self.visual_hi_id = 0 382 | endfunction 383 | 384 | " Restore unnamed register from paste buffer 385 | function! s:Cursor.restore_unnamed_register() dict 386 | call setreg('"', self.paste_buffer_text, self.paste_buffer_type) 387 | endfunction 388 | 389 | " Save contents of the unnamed register into paste buffer 390 | function! s:Cursor.save_unnamed_register() dict 391 | let self.paste_buffer_text = getreg('"') 392 | let self.paste_buffer_type = getregtype('"') 393 | endfunction 394 | 395 | "=============================================================================== 396 | " CursorManager class 397 | "=============================================================================== 398 | let s:CursorManager = {} 399 | 400 | " Constructor 401 | function! s:CursorManager.new() 402 | let obj = copy(self) 403 | " List of Cursors we're managing 404 | let obj.cursors = [] 405 | " Current index into the s:cursors array 406 | let obj.current_index = -1 407 | " This marks the starting cursor index into the s:cursors array 408 | let obj.starting_index = -1 409 | " We save some user settings when the plugin loads initially 410 | let obj.saved_settings = { 411 | \ 'virtualedit': &virtualedit, 412 | \ 'cursorline': &cursorline, 413 | \ 'lazyredraw': &lazyredraw, 414 | \ 'paste': &paste, 415 | \ 'clipboard': &clipboard, 416 | \ } 417 | " We save the window view when multicursor mode is entered 418 | let obj.saved_winview = [] 419 | " Track whether we started multicursor mode from calling multiple_cursors#find 420 | let obj.start_from_find = 0 421 | return obj 422 | endfunction 423 | 424 | " Clear all cursors and their highlights 425 | function! s:CursorManager.reset(restore_view, restore_setting, ...) dict 426 | if a:restore_view 427 | " Return the view back to the beginning 428 | if !empty(self.saved_winview) 429 | call winrestview(self.saved_winview) 430 | endif 431 | 432 | " If the cursor moved, just restoring the view could get confusing, let's 433 | " put the cursor at where the user left it. Only do this if we didn't start 434 | " from find mode 435 | if !self.is_empty() && !self.start_from_find 436 | call cursor(self.get(0).position) 437 | endif 438 | endif 439 | 440 | " Delete all cursors and clear their highlights. Don't do clearmatches() as 441 | " that will potentially interfere with other plugins 442 | if !self.is_empty() 443 | for i in range(self.size()) 444 | call self.remove_highlight(self.get(i).cursor_hi_id) 445 | call self.remove_highlight(self.get(i).visual_hi_id) 446 | endfor 447 | endif 448 | 449 | let self.cursors = [] 450 | let self.current_index = -1 451 | let self.starting_index = -1 452 | let self.saved_winview = [] 453 | let self.start_from_find = 0 454 | let s:char = '' 455 | let s:saved_char = '' 456 | if a:restore_setting 457 | call self.restore_user_settings() 458 | endif 459 | " Call after function if exists and only if action is canceled () 460 | if a:0 && s:before_function_called 461 | if exists('*Multiple_cursors_after') 462 | exe "call Multiple_cursors_after()" 463 | endif 464 | silent doautocmd User MultipleCursorsPost 465 | let s:before_function_called = 0 466 | endif 467 | endfunction 468 | 469 | " Returns 0 if it's not managing any cursors at the moment 470 | function! s:CursorManager.is_empty() dict 471 | return self.size() == 0 472 | endfunction 473 | 474 | " Returns the number of cursors it's managing 475 | function! s:CursorManager.size() dict 476 | return len(self.cursors) 477 | endfunction 478 | 479 | " Returns the current cursor 480 | function! s:CursorManager.get_current() dict 481 | return self.cursors[self.current_index] 482 | endfunction 483 | 484 | " Returns the cursor at index i 485 | function! s:CursorManager.get(i) dict 486 | return self.cursors[a:i] 487 | endfunction 488 | 489 | " Removes the current cursor and all its associated highlighting. Also update 490 | " the current index 491 | function! s:CursorManager.delete_current() dict 492 | call self.remove_highlight(self.get_current().cursor_hi_id) 493 | call self.remove_highlight(self.get_current().visual_hi_id) 494 | call remove(self.cursors, self.current_index) 495 | let self.current_index -= 1 496 | endfunction 497 | 498 | " Remove the highlighting if its matchid exists 499 | function! s:CursorManager.remove_highlight(hi_id) dict 500 | if a:hi_id 501 | " If the user did a matchdelete or a clearmatches, we don't want to barf if 502 | " the matchid is no longer valid 503 | silent! call matchdelete(a:hi_id) 504 | endif 505 | endfunction 506 | 507 | function! s:CursorManager.debug() dict 508 | let i = 0 509 | for c in self.cursors 510 | echom 'cursor #'.i.': pos='.string(c.position).' visual='.string(c.visual) 511 | let i+=1 512 | endfor 513 | echom 'input = '.s:char 514 | echom 'index = '.self.current_index 515 | echom 'pos = '.string(s:pos('.')) 516 | echom '''< = '.string(s:pos("'<")) 517 | echom '''> = '.string(s:pos("'>")) 518 | echom 'to mode = '.s:to_mode 519 | echom 'from mode = '.s:from_mode 520 | " echom 'special keys = '.string(s:special_keys) 521 | echom ' ' 522 | endfunction 523 | 524 | " Sync the current cursor to the current Vim cursor. This includes updating its 525 | " location, its highlight, and potentially its visual region. Return true if the 526 | " position changed, false otherwise 527 | function! s:CursorManager.update_current() dict 528 | let cur = self.get_current() 529 | if s:to_mode ==# 'v' || s:to_mode ==# 'V' 530 | " If we're in visual line mode, we need to go to visual mode before we can 531 | " update the visual region 532 | if s:to_mode ==# 'V' 533 | exec "normal! gvv\" 534 | endif 535 | " Sets the cursor at the right place 536 | exec "normal! gv\" 537 | call cur.update_visual_selection(s:get_visual_region(s:pos('.'))) 538 | elseif s:from_mode ==# 'v' || s:from_mode ==# 'V' 539 | " Save contents of unnamed register after each operation in Visual mode. 540 | " This should be executed after user input is processed, when unnamed 541 | " register already contains the text. 542 | call cur.save_unnamed_register() 543 | call cur.remove_visual_selection() 544 | elseif s:from_mode ==# 'i' && s:to_mode ==# 'n' && self.current_index != 0 545 | normal! h 546 | elseif s:from_mode ==# 'n' 547 | " Save contents of unnamed register after each operation in Normal mode. 548 | call cur.save_unnamed_register() 549 | endif 550 | let pos = s:pos('.') 551 | 552 | " If the total number of lines changed in the buffer, we need to potentially 553 | " adjust other cursor locations 554 | let vdelta = line('$') - s:saved_linecount 555 | if vdelta != 0 556 | if self.current_index != self.size() - 1 557 | let cur_column_offset = (cur.column() - col('.')) * -1 558 | let new_line_length = len(getline('.')) 559 | for i in range(self.current_index+1, self.size()-1) 560 | let hdelta = 0 561 | " Note: some versions of Vim don't like chaining function calls like 562 | " a.b().c(). For compatibility reasons, don't do it 563 | let c = self.get(i) 564 | " If there're other cursors on the same line, we need to adjust their 565 | " columns. This needs to happen before we adjust their line! 566 | if cur.line() == c.line() || cur.position == pos 567 | if vdelta > 0 568 | " Added a line 569 | let hdelta = cur_column_offset 570 | else 571 | " Removed a line 572 | let hdelta = new_line_length 573 | endif 574 | endif 575 | call c.move(vdelta, hdelta) 576 | endfor 577 | endif 578 | else 579 | " If the line length changes, for all the other cursors on the same line as 580 | " the current one, update their cursor location as well 581 | let hdelta = col('$') - cur.line_length 582 | " Only do this if we're still on the same line as before 583 | if hdelta != 0 && cur.line() == line('.') 584 | " Update all the cursor's positions that occur after the current cursor on 585 | " the same line 586 | if self.current_index != self.size() - 1 587 | for i in range(self.current_index+1, self.size()-1) 588 | let c = self.get(i) 589 | " Only do it for cursors on the same line 590 | if cur.line() == c.line() 591 | call c.move(0, hdelta) 592 | else 593 | " Early exit, if we're not on the same line, neither will any cursor 594 | " that come after this 595 | break 596 | endif 597 | endfor 598 | endif 599 | endif 600 | endif 601 | 602 | if cur.position == pos 603 | return 0 604 | endif 605 | call cur.update_position(pos) 606 | return 1 607 | endfunction 608 | 609 | " Advance to the next cursor 610 | function! s:CursorManager.next() dict 611 | let self.current_index = (self.current_index + 1) % self.size() 612 | endfunction 613 | 614 | " Start tracking cursor updates 615 | function! s:CursorManager.start_loop() dict 616 | let self.current_index = 0 617 | let self.starting_index = 0 618 | endfunction 619 | 620 | " Returns true if we're cycled through all the cursors 621 | function! s:CursorManager.loop_done() dict 622 | return self.current_index == self.starting_index 623 | endfunction 624 | 625 | " Tweak some user settings, and save our current window view. This is called 626 | " every time multicursor mode is entered. 627 | " virtualedit needs to be set to onemore for updates to work correctly 628 | " cursorline needs to be turned off for the cursor highlight to work on the line 629 | " where the real vim cursor is 630 | " lazyredraw needs to be turned on to prevent jerky screen behavior with many 631 | " cursors on screen 632 | " paste mode needs to be switched off since it turns off a bunch of features 633 | " that's critical for the plugin to function 634 | " clipboard should not have unnamed and unnamedplus otherwise plugin cannot 635 | " reliably use unnamed register ('"') 636 | function! s:CursorManager.initialize() dict 637 | let self.saved_settings['virtualedit'] = &virtualedit 638 | let self.saved_settings['cursorline'] = &cursorline 639 | let self.saved_settings['lazyredraw'] = &lazyredraw 640 | let self.saved_settings['paste'] = &paste 641 | let self.saved_settings['clipboard'] = &clipboard 642 | let &virtualedit = "onemore" 643 | let &cursorline = 0 644 | let &lazyredraw = 1 645 | let &paste = 0 646 | set clipboard-=unnamed clipboard-=unnamedplus 647 | " We could have already saved the view from multiple_cursors#find 648 | if !self.start_from_find 649 | let self.saved_winview = winsaveview() 650 | endif 651 | 652 | " Save contents and type of unnamed register upon entering multicursor mode 653 | " to restore it later when leaving mode 654 | let s:paste_buffer_temporary_text = getreg('"') 655 | let s:paste_buffer_temporary_type = getregtype('"') 656 | endfunction 657 | 658 | " Restore user settings. 659 | function! s:CursorManager.restore_user_settings() dict 660 | if !empty(self.saved_settings) 661 | let &virtualedit = self.saved_settings['virtualedit'] 662 | let &cursorline = self.saved_settings['cursorline'] 663 | let &lazyredraw = self.saved_settings['lazyredraw'] 664 | let &paste = self.saved_settings['paste'] 665 | let &clipboard = self.saved_settings['clipboard'] 666 | endif 667 | 668 | " Restore original contents and type of unnamed register. This method is 669 | " called from reset, which calls us only when restore_setting argument is 670 | " true, which happens only when we leave multicursor mode. This should be 671 | " symmetrical to saving of unnamed register upon the start of multicursor 672 | " mode. 673 | call setreg('"', s:paste_buffer_temporary_text, s:paste_buffer_temporary_type) 674 | endfunction 675 | 676 | " Reposition all cursors to the start or end of their region 677 | function! s:CursorManager.reposition_all_within_region(start) dict 678 | for c in self.cursors 679 | call c.update_position(c.saved_visual[a:start ? 0 : 1]) 680 | endfor 681 | endfunction 682 | 683 | " Reselect the current cursor's region in visual mode 684 | function! s:CursorManager.reapply_visual_selection() dict 685 | call s:select_in_visual_mode(self.get_current().visual) 686 | endfunction 687 | 688 | " Creates a new virtual cursor as 'pos' 689 | " Optionally a 'region' object can be passed in as second argument. If set, the 690 | " visual region of the cursor will be set to it 691 | " Return true if the cursor has been successfully added, false otherwise 692 | " Mode change: Normal -> Normal 693 | " Cursor change: None (TODO Should we set Vim's cursor to pos?) 694 | function! s:CursorManager.add(pos, ...) dict 695 | " Lazy init 696 | if self.is_empty() 697 | call self.initialize() 698 | endif 699 | 700 | " Don't add duplicates 701 | let i = 0 702 | for c in self.cursors 703 | if c.position == a:pos 704 | return 0 705 | endif 706 | let i+=1 707 | endfor 708 | 709 | let cursor = s:Cursor.new(a:pos) 710 | 711 | " Save the visual selection 712 | if a:0 > 0 713 | call cursor.update_visual_selection(a:1) 714 | endif 715 | 716 | call add(self.cursors, cursor) 717 | let self.current_index += 1 718 | return 1 719 | endfunction 720 | 721 | "=============================================================================== 722 | " Variables 723 | "=============================================================================== 724 | 725 | " This is the last user input that we're going to replicate, in its string form 726 | let s:char = '' 727 | " This is either `I` or `A`, as input in Visual mode, that we're going to use 728 | " to make the appropriate transition into Insert mode 729 | let s:saved_char = '' 730 | " This is the mode the user is in before s:char 731 | let s:from_mode = '' 732 | " This is the mode the user is in after s:char 733 | let s:to_mode = '' 734 | " This is the total number of lines in the buffer before processing s:char 735 | let s:saved_linecount = -1 736 | " This is used to apply the highlight fix. See s:apply_highight_fix() 737 | let s:saved_line = 0 738 | " This is the number of cursor locations where we detected an input that we 739 | " cannot play back 740 | let s:bad_input = 0 741 | " Singleton cursor manager instance 742 | let s:cm = s:CursorManager.new() 743 | 744 | "=============================================================================== 745 | " Utility functions 746 | "=============================================================================== 747 | 748 | " Return the position of the input marker as a two element array. First element 749 | " is the line number, second element is the column number 750 | function! s:pos(mark) 751 | let pos = getpos(a:mark) 752 | return [pos[1], pos[2]] 753 | endfunction 754 | 755 | " Return the region covered by the input markers as a two element array. First 756 | " element is the position of the start marker, second element is the position of 757 | " the end marker 758 | function! s:region(start_mark, end_mark) 759 | return [s:pos(a:start_mark), s:pos(a:end_mark)] 760 | endfunction 761 | 762 | " Exit visual mode and go back to normal mode 763 | " The reason for the additional gv\ is that it allows the cursor to stay 764 | " on where it was before exiting 765 | " Mode change: Normal -> Normal or Visual -> Normal 766 | " Cursor change: If in visual mode, changed to exactly where it was on screen in 767 | " visual mode. If in normal mode, changed to where the cursor was when the last 768 | " visual selection ended 769 | function! s:exit_visual_mode() 770 | exec "normal! \gv\" 771 | 772 | " Call before function if exists only once until it is canceled () 773 | if exists('*Multiple_cursors_before') && !s:before_function_called 774 | exe "call Multiple_cursors_before()" 775 | let s:before_function_called = 1 776 | endif 777 | endfunction 778 | 779 | " Visually select input region, where region is an array containing the start 780 | " and end position. If start is after end, the selection simply goes backwards. 781 | " Typically m<, m>, and gv would be a simple way of accomplishing this, but on 782 | " some systems, the m< and m> marks are not supported. Note that v`` has random 783 | " behavior if `` is the same location as the cursor location. 784 | " Mode change: Normal -> Visual 785 | " Cursor change: Set to end of region 786 | " TODO: Refactor this and s:update_visual_markers 787 | " FIXME: By using m` we're destroying the user's jumplist. We should use a 788 | " different mark and use :keepjump 789 | function! s:select_in_visual_mode(region) 790 | if a:region[0] == a:region[1] 791 | normal! v 792 | else 793 | call cursor(a:region[1]) 794 | normal! m` 795 | call cursor(a:region[0]) 796 | normal! v`` 797 | endif 798 | 799 | " Unselect and reselect it again to properly set the '< and '> markers 800 | exec "normal! \gv" 801 | endfunction 802 | 803 | " Update '< and '> to the input region 804 | " Mode change: Normal -> Normal 805 | " Cursor change: Set to the end of the region 806 | function! s:update_visual_markers(region) 807 | if a:region[0] == a:region[1] 808 | normal! v 809 | else 810 | call cursor(a:region[1]) 811 | normal! m` 812 | call cursor(a:region[0]) 813 | normal! v`` 814 | endif 815 | call s:exit_visual_mode() 816 | endfunction 817 | 818 | " Finds the next occurrence of the input text in the current buffer. 819 | " Search is case sensitive 820 | " Mode change: Normal -> Normal 821 | " Cursor change: Set to the end of the match 822 | function! s:find_next(text) 823 | let pattern = substitute(escape(a:text, '\'), '\n', '\\n', 'g') 824 | if s:use_word_boundary == 1 825 | let pattern = '\<'.pattern.'\>' 826 | endif 827 | let pattern = '\V\C'.pattern 828 | call search(pattern) 829 | let start = s:pos('.') 830 | call search(pattern, 'ce') 831 | let end = s:pos('.') 832 | return [start, end] 833 | endfunction 834 | 835 | " Highlight the position using the cursor highlight group 836 | function! s:highlight_cursor(pos) 837 | " Give cursor highlight high priority, to overrule visual selection 838 | return matchadd(s:hi_group_cursor, '\%'.a:pos[0].'l\%'.a:pos[1].'c', 99999) 839 | endfunction 840 | 841 | " Compare two position arrays. Return a negative value if lhs occurs before rhs, 842 | " positive value if after, and 0 if they are the same. 843 | function! s:compare_pos(l, r) 844 | " If number lines are the same, compare columns 845 | return a:l[0] ==# a:r[0] ? a:l[1] - a:r[1] : a:l[0] - a:r[0] 846 | endfunction 847 | 848 | " Highlight the area bounded by the input region. The logic here really stinks, 849 | " it's frustrating that Vim doesn't have a built in easier way to do this. None 850 | " of the \%V or \%'m solutions work because we need the highlighting to stay for 851 | " multiple places. 852 | function! s:highlight_region(region) 853 | let s = sort(copy(a:region), "s:compare_pos") 854 | if s:to_mode ==# 'V' 855 | let pattern = '\%>'.(s[0][0]-1).'l\%<'.(s[1][0]+1).'l.*\ze.\_$' 856 | else 857 | if (s[0][0] == s[1][0]) 858 | " Same line 859 | let pattern = '\%'.s[0][0].'l\%>'.(s[0][1]-1).'c.*\%<'.(s[1][1]+1).'c.' 860 | else 861 | " Two lines 862 | let s1 = '\%'.s[0][0].'l.\%>'.s[0][1].'c.*' 863 | let s2 = '\%'.s[1][0].'l.*\%<'.s[1][1].'c..' 864 | let pattern = s1.'\|'.s2 865 | " More than two lines 866 | if (s[1][0] - s[0][0] > 1) 867 | let pattern = pattern.'\|\%>'.s[0][0].'l\%<'.s[1][0].'l.*\ze.\_$' 868 | endif 869 | endif 870 | endif 871 | return matchadd(s:hi_group_visual, pattern) 872 | endfunction 873 | 874 | " Perform the operation that's necessary to revert us from one mode to another 875 | function! s:revert_mode(from, to) 876 | if a:to ==# 'v' 877 | call s:cm.reapply_visual_selection() 878 | elseif a:to ==# 'V' 879 | call s:cm.reapply_visual_selection() 880 | normal! V 881 | elseif a:to ==# 'n' && a:from ==# 'i' 882 | stopinsert 883 | endif 884 | endfunction 885 | 886 | " Consume all the additional character the user typed between the last 887 | " getchar() and here, to avoid potential race condition. 888 | let s:saved_keys = "" 889 | function! s:feedkeys(keys) 890 | while 1 891 | let c = getchar(0) 892 | let char_type = type(c) 893 | " Checking type is important, when strings are compared with integers, 894 | " strings are always converted to ints, and all strings are equal to 0 895 | if char_type == 0 896 | if c == 0 897 | break 898 | else 899 | let s:saved_keys .= nr2char(c) 900 | endif 901 | elseif char_type == 1 " char with more than 8 bits (as string) 902 | let s:saved_keys .= c 903 | endif 904 | endwhile 905 | call feedkeys(a:keys) 906 | endfunction 907 | 908 | " Take the user input and apply it at every cursor 909 | function! s:process_user_input() 910 | " Grr this is frustrating. In Insert mode, between the feedkey call and here, 911 | " the current position could actually CHANGE for some odd reason. Forcing a 912 | " position reset here 913 | let cursor_position = s:cm.get_current() 914 | call cursor(cursor_position.position) 915 | 916 | " Before applying the user input, we need to revert back to the mode the user 917 | " was in when the input was entered 918 | call s:revert_mode(s:to_mode, s:from_mode) 919 | 920 | " Update the line length BEFORE applying any actions. TODO(terryma): Is there 921 | " a better place to do this? 922 | " let cursor_position = s:cm.get_current() 923 | call cursor_position.update_line_length() 924 | let s:saved_linecount = line('$') 925 | 926 | " Restore unnamed register only in Normal mode. This should happen before user 927 | " input is processed. 928 | if s:from_mode ==# 'n' || s:from_mode ==# 'v' || s:from_mode ==# 'V' 929 | call cursor_position.restore_unnamed_register() 930 | endif 931 | 932 | " Apply the user input. Note that the above could potentially change mode, we 933 | " use the mapping below to help us determine what the new mode is 934 | " Note that it's possible that \(multiple-cursors-apply) never gets called, we have a 935 | " detection mechanism using \(multiple-cursors-detect). See its documentation for more details 936 | 937 | " Assume that input is not valid 938 | let s:valid_input = 0 939 | 940 | " If we're coming from insert mode or going into insert mode, always chain the 941 | " undos together. 942 | " FIXME(terryma): Undo always places the cursor at the beginning of the line. 943 | " Figure out why. 944 | if s:from_mode ==# 'i' || s:to_mode ==# 'i' 945 | silent! undojoin | call s:feedkeys(s:char."\(multiple-cursors-apply)") 946 | else 947 | call s:feedkeys(s:char."\(multiple-cursors-apply)") 948 | endif 949 | 950 | " Even when s:char produces invalid input, this method is always called. The 951 | " 't' here is important 952 | call feedkeys("\(multiple-cursors-detect)", 't') 953 | endfunction 954 | 955 | " This method is always called during fanout, even when a bad user input causes 956 | " s:apply_user_input_next to not be called. We detect that and force the method 957 | " to be called to continue the fanout process 958 | function! s:detect_bad_input() 959 | if !s:valid_input 960 | " To invoke the appropriate `(multiple-cursors-apply)` mapping, we 961 | " need to revert back to the mode the user was in when the input was entered 962 | call s:revert_mode(s:to_mode, s:from_mode) 963 | " We ignore the bad input and force invoke s:apply_user_input_next 964 | call feedkeys("\(multiple-cursors-apply)") 965 | let s:bad_input += 1 966 | endif 967 | endfunction 968 | 969 | " Complete transition into Insert mode when `I` or `A` is input in Visual mode 970 | function! s:handle_visual_IA_to_insert() 971 | if !empty(s:saved_char) && s:char =~# 'v\|V' && s:to_mode ==# 'n' 972 | if s:saved_char ==# 'I' 973 | call s:cm.reposition_all_within_region(1) 974 | endif 975 | call feedkeys(tolower(s:saved_char)) 976 | let s:saved_char = '' 977 | endif 978 | endfunction 979 | 980 | " Begin transition into Insert mode when `I` or `A` is input in Visual mode 981 | function! s:handle_visual_IA_to_normal() 982 | if s:char =~# 'I\|A' && s:from_mode =~# 'v\|V' 983 | let s:saved_char = s:char 984 | let s:char = s:from_mode " spoof a 'v' or 'V' input to transiton from Visual into Normal mode 985 | endif 986 | endfunction 987 | 988 | " Apply the user input at the next cursor location 989 | function! s:apply_user_input_next(mode) 990 | let s:valid_input = 1 991 | 992 | " Save the current mode, only if we haven't already 993 | if empty(s:to_mode) 994 | let s:to_mode = a:mode 995 | if s:to_mode ==# 'v' 996 | if visualmode() ==# 'V' 997 | let s:to_mode = 'V' 998 | endif 999 | endif 1000 | endif 1001 | 1002 | " Update the current cursor's information 1003 | let changed = s:cm.update_current() 1004 | 1005 | " Advance the cursor index 1006 | call s:cm.next() 1007 | 1008 | " We're done if we're made the full round 1009 | if s:cm.loop_done() 1010 | if s:to_mode ==# 'v' || s:to_mode ==# 'V' 1011 | " This is necessary to set the "'<" and "'>" markers properly 1012 | call s:update_visual_markers(s:cm.get_current().visual) 1013 | endif 1014 | call feedkeys("\(multiple-cursors-wait)") 1015 | call s:handle_visual_IA_to_insert() 1016 | else 1017 | " Continue to next 1018 | call feedkeys("\(multiple-cursors-input)") 1019 | endif 1020 | endfunction 1021 | 1022 | " If pos is equal to the left side of the visual selection, the region start 1023 | " from end to start 1024 | function! s:get_visual_region(pos) 1025 | let left = s:pos("'<") 1026 | let right = s:pos("'>") 1027 | if a:pos == left 1028 | let region = [right, left] 1029 | else 1030 | let region = [left, right] 1031 | endif 1032 | return region 1033 | endfunction 1034 | 1035 | function! s:strpart(s, i, l) 1036 | if a:l == 0 1037 | return '' 1038 | endif 1039 | let [s, l] = ['', 0] 1040 | for c in split(a:s[a:i :], '\zs') 1041 | let s .= c 1042 | let l += len(c) 1043 | if l >= a:l 1044 | break 1045 | endif 1046 | endfor 1047 | return s 1048 | endfunction 1049 | 1050 | " Return the content of the buffer between the input region. This is used to 1051 | " find the next match in the buffer 1052 | " Mode change: Normal -> Normal 1053 | " Cursor change: None 1054 | function! s:get_text(region) 1055 | let lines = getline(a:region[0][0], a:region[1][0]) 1056 | let lines[-1] = s:strpart(lines[-1], 0, a:region[1][1]) 1057 | let lines[0] = lines[0][a:region[0][1] - 1:] 1058 | return join(lines, "\n") 1059 | endfunction 1060 | 1061 | " Wrapper around getchar() that returns the string representation of the user 1062 | " input 1063 | function! s:get_char(...) 1064 | let c = (a:0 == 0) ? getchar() : getchar(a:1) 1065 | " If the character is a number, then it's not a special key 1066 | if type(c) == 0 1067 | let c = nr2char(c) 1068 | endif 1069 | return c 1070 | endfunction 1071 | 1072 | " Quits multicursor mode and clears all cursors. Return true if exited 1073 | " successfully. 1074 | function! s:exit() 1075 | if s:char !=# g:multi_cursor_quit_key 1076 | return 0 1077 | endif 1078 | let exit = 0 1079 | if s:from_mode ==# 'n' 1080 | let exit = 1 1081 | elseif (s:from_mode ==# 'v' || s:from_mode ==# 'V') && 1082 | \ g:multi_cursor_exit_from_visual_mode 1083 | let exit = 1 1084 | elseif s:from_mode ==# 'i' && g:multi_cursor_exit_from_insert_mode 1085 | stopinsert 1086 | let exit = 1 1087 | endif 1088 | if exit 1089 | call s:cm.reset(1, 1, 1) 1090 | return 1 1091 | endif 1092 | return 0 1093 | endfunction 1094 | 1095 | " These keys don't get faned out to all cursor locations. Instead, they're used 1096 | " to add new / remove existing cursors 1097 | " Precondition: The function is only called when the keys and mode respect the 1098 | " setting in s:special_keys 1099 | function! s:handle_special_key(key, mode) 1100 | " Use feedkeys here instead of calling the function directly to prevent 1101 | " increasing the call stack, since feedkeys execute after the current call 1102 | " finishes 1103 | if a:key == g:multi_cursor_next_key 1104 | if s:use_word_boundary == 1 1105 | call s:feedkeys("\(multiple-cursors-new-word)") 1106 | else 1107 | call s:feedkeys("\(multiple-cursors-new)") 1108 | endif 1109 | elseif a:key == g:multi_cursor_prev_key 1110 | call s:feedkeys("\(multiple-cursors-prev)") 1111 | elseif a:key == g:multi_cursor_skip_key 1112 | call s:feedkeys("\(multiple-cursors-skip)") 1113 | endif 1114 | endfunction 1115 | 1116 | " The last line where the normal Vim cursor is always seems to highlighting 1117 | " issues if the cursor is on the last column. Vim's cursor seems to override the 1118 | " highlight of the virtual cursor. This won't happen if the virtual cursor isn't 1119 | " the last character on the line. This is a hack to add an empty space on the 1120 | " Vim cursor line right before we do the redraw, we'll revert the change 1121 | " immedidately after the redraw so the change should not be intrusive to the 1122 | " user's buffer content 1123 | function! s:apply_highlight_fix() 1124 | " Only do this if we're on the last character of the line 1125 | if col('.') == col('$') 1126 | let s:saved_line = getline('.') 1127 | if s:from_mode ==# 'i' 1128 | silent! undojoin | call setline('.', s:saved_line.' ') 1129 | else 1130 | call setline('.', s:saved_line.' ') 1131 | endif 1132 | endif 1133 | endfunction 1134 | 1135 | " Revert the fix if it was applied earlier 1136 | function! s:revert_highlight_fix() 1137 | if type(s:saved_line) == 1 1138 | if s:from_mode ==# 'i' 1139 | silent! undojoin | call setline('.', s:saved_line) 1140 | else 1141 | call setline('.', s:saved_line) 1142 | endif 1143 | endif 1144 | let s:saved_line = 0 1145 | endfunction 1146 | 1147 | let s:retry_keys = "" 1148 | function! s:display_error() 1149 | if s:bad_input == s:cm.size() 1150 | \ && ((s:from_mode ==# 'n' && has_key(g:multi_cursor_normal_maps, s:char[0])) 1151 | \ || (s:from_mode =~# 'v\|V' && has_key(g:multi_cursor_visual_maps, s:char[0]))) 1152 | " we couldn't replay it anywhere but we're told it's the beginning of a 1153 | " multi-character map like the `d` in `dw` 1154 | let s:retry_keys = s:char 1155 | else 1156 | let s:retry_keys = "" 1157 | if s:bad_input > 0 1158 | echohl ErrorMsg | 1159 | \ echo "Key '".s:char."' cannot be replayed at ". 1160 | \ s:bad_input." cursor location".(s:bad_input == 1 ? '' : 's') | 1161 | \ echohl Normal 1162 | endif 1163 | endif 1164 | let s:bad_input = 0 1165 | endfunction 1166 | 1167 | let s:latency_debug_file = '' 1168 | function! s:start_latency_measure() 1169 | if g:multi_cursor_debug_latency 1170 | let s:start_time = reltime() 1171 | endif 1172 | endfunction 1173 | 1174 | function! s:skip_latency_measure() 1175 | if g:multi_cursor_debug_latency 1176 | let s:skip_latency_measure = 1 1177 | endif 1178 | endfunction 1179 | 1180 | function! s:end_latency_measure() 1181 | if g:multi_cursor_debug_latency && !empty(s:char) 1182 | if empty(s:latency_debug_file) 1183 | let s:latency_debug_file = tempname() 1184 | exec 'redir >> '.s:latency_debug_file 1185 | silent! echom "Starting latency debug at ".reltimestr(reltime()) 1186 | redir END 1187 | endif 1188 | 1189 | if !s:skip_latency_measure 1190 | exec 'redir >> '.s:latency_debug_file 1191 | silent! echom "Processing '".s:char."' took ".string(str2float(reltimestr(reltime(s:start_time)))*1000).' ms in '.s:cm.size().' cursors. mode = '.s:from_mode 1192 | redir END 1193 | endif 1194 | endif 1195 | let s:skip_latency_measure = 0 1196 | endfunction 1197 | 1198 | function! s:get_time_in_ms() 1199 | return str2nr(substitute(reltimestr(reltime()), '\.\(...\).*', '\1', '')) 1200 | endfunction 1201 | 1202 | function! s:last_char() 1203 | return s:char[len(s:char)-1] 1204 | endfunction 1205 | 1206 | function! s:wait_for_user_input(mode) 1207 | call s:display_error() 1208 | 1209 | let s:from_mode = a:mode 1210 | if empty(a:mode) 1211 | let s:from_mode = s:to_mode 1212 | endif 1213 | let s:to_mode = '' 1214 | 1215 | " Right before redraw, apply the highlighting bug fix 1216 | call s:apply_highlight_fix() 1217 | 1218 | redraw 1219 | 1220 | " Immediately revert the change to leave the user's buffer unchanged 1221 | call s:revert_highlight_fix() 1222 | 1223 | call s:end_latency_measure() 1224 | 1225 | let s:char = s:retry_keys . s:saved_keys 1226 | if len(s:saved_keys) == 0 1227 | let s:char .= s:get_char() 1228 | call s:handle_visual_IA_to_normal() 1229 | else 1230 | let s:saved_keys = "" 1231 | endif 1232 | 1233 | " ambiguous mappings are not supported; e.g.: 1234 | " imap jj JJ 1235 | " imap jjj JJJ 1236 | " will always trigger the 'jj' mapping 1237 | if s:from_mode ==# 'i' && mapcheck(s:char, "i") != "" && g:multi_cursor_support_imap 1238 | let map_dict = {} 1239 | let s_time = s:get_time_in_ms() 1240 | while 1 1241 | let map_dict = maparg(s:char, "i", 0, 1) 1242 | " break if chars exactly match mapping 1243 | if map_dict != {} 1244 | if get(map_dict, 'expr', 0) 1245 | " handle case where {rhs} is a function 1246 | exec 'let char_mapping = ' . map_dict['rhs'] 1247 | else 1248 | let char_mapping = maparg(s:char, "i") 1249 | endif 1250 | " handle case where mapping is 1251 | exec 'let s:char = "'.substitute(char_mapping, '<', '\\<', 'g').'"' 1252 | break 1253 | endif 1254 | " break if chars don't match beginning of mapping anymore 1255 | if mapcheck(s:char, "i") == "" 1256 | break 1257 | endif 1258 | if s:get_time_in_ms() > (s_time + &timeoutlen) 1259 | break 1260 | endif 1261 | let new_char = s:get_char(0) 1262 | let s:char .= new_char 1263 | if new_char == '' 1264 | sleep 50m 1265 | endif 1266 | endwhile 1267 | elseif s:from_mode !=# 'i' && s:char[0] ==# ":" 1268 | call feedkeys(s:char) 1269 | call s:cm.reset(1, 1, 1) 1270 | return 1271 | elseif s:from_mode ==# 'n' || s:from_mode =~# 'v\|V' 1272 | while match(s:last_char(), "\\d") == 0 1273 | if match(s:char, '\(^\|\a\)0') == 0 1274 | " fixes an edge case concerning the `0` key. 1275 | " The 0 key behaves differently from [1-9]. 1276 | " It's consumed immediately when it is the 1277 | " first key typed while we're waiting for input. 1278 | " References: issue #152, pull #241 1279 | break 1280 | endif 1281 | let s:char .= s:get_char() 1282 | endwhile 1283 | endif 1284 | 1285 | call s:start_latency_measure() 1286 | 1287 | " Clears any echoes we might've added 1288 | normal! : 1289 | 1290 | " add chars to s:char if it start like a special/quit key 1291 | let is_special_key = 0 1292 | let sk_list = get(s:special_keys, s:from_mode, []) 1293 | let is_special_key = (index(sk_list, s:char) != -1) 1294 | let is_quit_key = 0 1295 | let s_time = s:get_time_in_ms() 1296 | while 1 1297 | let start_special_key = (index(map(sk_list[:], 'v:val[0:len(s:char)-1] == s:char'), 1) > -1) 1298 | let start_quit_key = (g:multi_cursor_quit_key[0:len(s:char)-1] == s:char) 1299 | if start_special_key == 0 && start_quit_key == 0 1300 | break 1301 | else 1302 | let is_special_key = (index(sk_list, s:char) != -1) 1303 | let is_quit_key = (g:multi_cursor_quit_key == s:char) 1304 | if is_special_key == 1 || is_quit_key == 1 1305 | break 1306 | else 1307 | if s:get_time_in_ms() > (s_time + &timeoutlen) 1308 | break 1309 | endif 1310 | let new_char = s:get_char(0) 1311 | let s:char .= new_char 1312 | if new_char == '' 1313 | sleep 50m 1314 | endif 1315 | endif 1316 | end 1317 | endwhile 1318 | 1319 | if s:exit() 1320 | return 1321 | endif 1322 | 1323 | " If the key is a special key and we're in the right mode, handle it 1324 | if is_special_key == 1 1325 | call s:handle_special_key(s:char, s:from_mode) 1326 | call s:skip_latency_measure() 1327 | else 1328 | call s:cm.start_loop() 1329 | call s:feedkeys("\(multiple-cursors-input)") 1330 | endif 1331 | endfunction 1332 | --------------------------------------------------------------------------------