Skip to content

Commit 35f2bcd

Browse files
committed
Chain undos together in insert mode. Prevent ghostly cursors.
This commit provides basic chaining of undos in insert mode. Previously, undos of changes made in multicursor mode are done one character at a time. This change should chain all inserts into a single undo block, and undo them together. This fixes #22. We're also adding support to detect non-terminating single key commands. For example, previously, 'd' would break multicursor mode and cause these ghostly cursors to linger on screen, where the only way to get rid of them is to press `<C-n>` somewhere to start a new multicursor mode. Now, these unterminated commands are detected and simply cause a nop, execution continues and we're left in multicursor mode. This paves the way to support multikey commands in the future. This fixes #28.
1 parent db7fb78 commit 35f2bcd

File tree

5 files changed

+187
-18
lines changed

5 files changed

+187
-18
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.0 (04/24/2013)
2+
3+
Bugfixes:
4+
- Fix inconsistent undo behavior. Changes made in multicursor insert mode are now undone together. This fixes #22.
5+
- 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.
6+
17
## 1.16 (04/23/2013)
28

39
Features:

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Two additional keys are also mapped:
3838

3939
You can also add multiple cursors using a regular expression. The command `MultipleCursorsFind` accepts a range and a pattern, and it will create a virtual cursor at the end of every match within the range. If no range is passed in, then it defaults to the entire buffer.
4040

41-
**NOTE:** The plugin is still somewhat buggy, if at any time you have lingering cursors on screen, you can press `Ctrl-n` in Normal mode and it will remove all prior cursors before starting a new one.
41+
**NOTE:** If at any time you have lingering cursors on screen, you can press `Ctrl-n` in Normal mode and it will remove all prior cursors before starting a new one.
4242

4343
## Mapping
4444
Out of the box, only the single key `Ctrl-n` is mapped in regular Vim's Normal mode and Visual mode to provide the functionality mentioned above. `Ctrl-n`, `Ctrl-p`, `Ctrl-x`, and `<Esc>` are mapped in the special multicursor mode once you've added at least one virtual cursor to the buffer. If you don't like the plugin taking over your favorite key bindings, you can turn off the default with
@@ -88,11 +88,8 @@ highlight link multiple_cursors_visual Visual
8888
## Issues
8989
- Multi key commands like `ciw` do not work at the moment
9090
- All user input typed before Vim is able to fan out the last operation to all cursors is lost. This is a implementation decision to keep the input perfectly synced in all locations, at the cost of potentially losing user input.
91-
- Single key commands that do not terminate properly cause unexpected behavior. For example, if the cursor is on the first character in the buffer and 'b' is pressed.
92-
- Undo behavior is unpredictable
9391
- Performance in terminal vim degrades significantly with more cursors
9492
- Select mode is not implemented
95-
- Buggy when `wrap` is turned on
9693

9794
## Changelog
9895
See [CHANGELOG.md](CHANGELOG.md)

autoload/multiple_cursors.vim

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ inoremap <silent> <Plug>(a) <C-o>:call <SID>apply_user_input_next('i')<CR>
5858
nnoremap <silent> <Plug>(a) :call <SID>apply_user_input_next('n')<CR>
5959
xnoremap <silent> <Plug>(a) :<C-u>call <SID>apply_user_input_next('v')<CR>
6060
61+
inoremap <silent> <Plug>(d) <C-o>:call <SID>detect_bad_input()<CR>
62+
nnoremap <silent> <Plug>(d) :call <SID>detect_bad_input()<CR>
63+
xnoremap <silent> <Plug>(d) :<C-u>call <SID>detect_bad_input()<CR>
64+
65+
inoremap <silent> <Plug>(w) <C-o>:call <SID>wait_for_user_input('')<CR>
66+
nnoremap <silent> <Plug>(w) :call <SID>wait_for_user_input('')<CR>
67+
xnoremap <silent> <Plug>(w) :<C-u>call <SID>wait_for_user_input('')<CR>
68+
6169
" Note that although these mappings are seemingly triggerd from Visual mode,
6270
" they are in fact triggered from Normal mode. We quit visual mode to allow the
6371
" virtual highlighting to take over
@@ -556,6 +564,7 @@ function! s:CursorManager.add(pos, ...) dict
556564
let self.current_index += 1
557565
return 1
558566
endfunction
567+
559568
"===============================================================================
560569
" Variables
561570
"===============================================================================
@@ -570,6 +579,9 @@ let s:to_mode = ''
570579
let s:saved_linecount = -1
571580
" This is used to apply the highlight fix. See s:apply_highight_fix()
572581
let s:saved_line = 0
582+
" This is the number of cursor locations where we detected an input that we
583+
" cannot play back
584+
let s:bad_input = 0
573585
" Singleton cursor manager instance
574586
let s:cm = s:CursorManager.new()
575587

@@ -745,12 +757,42 @@ function! s:process_user_inut()
745757

746758
" Apply the user input. Note that the above could potentially change mode, we
747759
" use the mapping below to help us determine what the new mode is
748-
" FIXME(terryma): It's possible that \<Plug>(a) never gets called
749-
call s:feedkeys(s:char."\<Plug>(a)")
760+
" Note that it's possible that \<Plug>(a) never gets called, we have a
761+
" detection mechanism using \<Plug>(d). See its documentation for more details
762+
763+
" Assume that input is not valid
764+
let s:valid_input = 0
765+
766+
" If we're coming from insert mode or going into insert mode, always chain the
767+
" undos together.
768+
" FIXME(terryma): Undo always places the cursor at the beginning of the line.
769+
" Figure out why.
770+
if s:from_mode ==# 'i' || s:to_mode ==# 'i'
771+
silent! undojoin | call feedkeys(s:char."\<Plug>(a)")
772+
else
773+
call feedkeys(s:char."\<Plug>(a)")
774+
endif
775+
776+
" Even when s:char produces invalid input, this method is always called. The
777+
" 't' here is important
778+
call feedkeys("\<Plug>(d)", 't')
779+
endfunction
780+
781+
" This method is always called during fanout, even when a bad user input causes
782+
" s:apply_user_input_next to not be called. We detect that and force the method
783+
" to be called to continue the fanout process
784+
function! s:detect_bad_input()
785+
if !s:valid_input
786+
" We ignore the bad input and force invoke s:apply_user_input_next
787+
call feedkeys("\<Plug>(a)")
788+
let s:bad_input += 1
789+
endif
750790
endfunction
751791

752792
" Apply the user input at the next cursor location
753793
function! s:apply_user_input_next(mode)
794+
let s:valid_input = 1
795+
754796
" Save the current mode, only if we haven't already
755797
if empty(s:to_mode)
756798
let s:to_mode = a:mode
@@ -776,10 +818,10 @@ function! s:apply_user_input_next(mode)
776818
" This is necessary to set the "'<" and "'>" markers properly
777819
call s:update_visual_markers(s:cm.get_current().visual)
778820
endif
779-
call s:wait_for_user_input(s:to_mode)
821+
call feedkeys("\<Plug>(w)")
780822
else
781823
" Continue to next
782-
call s:process_user_inut()
824+
call feedkeys("\<Plug>(i)")
783825
endif
784826
endfunction
785827

@@ -869,22 +911,45 @@ function! s:apply_highlight_fix()
869911
" Only do this if we're on the last character of the line
870912
if col('.') == col('$')
871913
let s:saved_line = getline('.')
872-
call setline('.', s:saved_line.' ')
914+
if s:from_mode ==# 'i'
915+
silent! undojoin | call setline('.', s:saved_line.' ')
916+
else
917+
call setline('.', s:saved_line.' ')
918+
endif
873919
endif
874920
endfunction
875921

876922
" Revert the fix if it was applied earlier
877923
function! s:revert_highlight_fix()
878924
if type(s:saved_line) == 1
879-
call setline('.', s:saved_line)
925+
if s:from_mode ==# 'i'
926+
silent! undojoin | call setline('.', s:saved_line)
927+
else
928+
call setline('.', s:saved_line)
929+
endif
880930
endif
881931
let s:saved_line = 0
882932
endfunction
883933

934+
function! s:display_error()
935+
if s:bad_input > 0
936+
echohl ErrorMsg |
937+
\ echo "Key '".s:char."' cannot be replayed at ".
938+
\ s:bad_input." cursor location".(s:bad_input == 1 ? '' : 's') |
939+
\ echohl Normal
940+
endif
941+
let s:bad_input = 0
942+
endfunction
943+
884944
function! s:wait_for_user_input(mode)
885945
let s:from_mode = a:mode
946+
if empty(a:mode)
947+
let s:from_mode = s:to_mode
948+
endif
886949
let s:to_mode = ''
887950

951+
call s:display_error()
952+
888953
" Right before redraw, apply the highlighting bug fix
889954
call s:apply_highlight_fix()
890955

doc/multiple_cursors.txt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ You can also add multiple cursors using a regular expression. The command
6666
virtual cursor at the end of every match within the range. If no range is
6767
passed in, then it defaults to the entire buffer.
6868

69-
NOTE: The plugin is still somewhat buggy, if at any time you have
70-
lingering cursors on screen, you can press CTRL-N in Normal mode and it will
71-
remove all prior cursors before starting a new one.
69+
NOTE: If at any time you have lingering cursors on screen, you can press
70+
CTRL-N in Normal mode and it will remove all prior cursors before starting a
71+
new one.
7272

7373
==============================================================================
7474
3. Mappings *multiple-cursors-mappings*
@@ -160,13 +160,8 @@ like the following in your vimrc: >
160160
cursors is lost. This is a implementation decision to keep the input
161161
perfectly synced in all locations, at the cost of potentially losing user
162162
input.
163-
- Single key commands that do not terminate properly cause unexpected
164-
behavior. For example, if the cursor is on the first character in the buffer
165-
and 'b' is pressed.
166-
- Undo behavior is unpredictable
167163
- Performance in terminal vim degrades significantly with more cursors
168164
- Select mode is not implemented
169-
- Buggy when wrap is turned on
170165

171166
==============================================================================
172167
6. Contributing *multiple-cursors-contributing*

spec/multiple_cursors_spec.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ def type(string)
9090
EOF
9191
end
9292

93+
specify "#new line in insert mode middle of line" do
94+
before <<-EOF
95+
hello world
96+
hello world
97+
EOF
98+
99+
type '<C-n><C-n>vlxi<cr><Esc>'
100+
101+
after <<-EOF
102+
hello
103+
world
104+
hello
105+
world
106+
EOF
107+
end
108+
93109
specify "#normal mode 'o'" do
94110
before <<-EOF
95111
hello
@@ -150,4 +166,94 @@ def type(string)
150166
hi!
151167
EOF
152168
end
169+
170+
specify "#skip key" do
171+
before <<-EOF
172+
hello
173+
hello
174+
hello
175+
EOF
176+
177+
type '<C-n><C-n><C-x>cworld<Esc>'
178+
179+
after <<-EOF
180+
world
181+
hello
182+
world
183+
EOF
184+
end
185+
186+
specify "#prev key" do
187+
before <<-EOF
188+
hello
189+
hello
190+
hello
191+
EOF
192+
193+
type '<C-n><C-n><C-n><C-p>cworld<Esc>'
194+
195+
after <<-EOF
196+
world
197+
world
198+
hello
199+
EOF
200+
end
201+
202+
specify "#normal mode 'I'" do
203+
before <<-EOF
204+
hello
205+
hello
206+
EOF
207+
208+
type '<C-n><C-n>vIworld <Esc>'
209+
210+
after <<-EOF
211+
world hello
212+
world hello
213+
EOF
214+
end
215+
216+
specify "#normal mode 'A'" do
217+
before <<-EOF
218+
hello
219+
hello
220+
EOF
221+
222+
type '<C-n><C-n>vA world<Esc>'
223+
224+
after <<-EOF
225+
hello world
226+
hello world
227+
EOF
228+
end
229+
230+
specify "#undo" do
231+
before <<-EOF
232+
hello
233+
hello
234+
EOF
235+
236+
type '<C-n><C-n>cworld<Esc>u'
237+
238+
after <<-EOF
239+
hello
240+
hello
241+
EOF
242+
end
243+
244+
# 'd' is an operator pending command, which are not supported at the moment.
245+
# This should result in a nop, but we should still remain in multicursor mode.
246+
specify "#normal mode 'd'" do
247+
before <<-EOF
248+
hello
249+
hello
250+
EOF
251+
252+
type '<C-n><C-n>vdx<Esc>'
253+
254+
after <<-EOF
255+
hell
256+
hell
257+
EOF
258+
end
153259
end

0 commit comments

Comments
 (0)