2013-04-26 12:17:22 -04:00
"===============================================================================
" Initialization
"===============================================================================
" Tweak key settings. If the key is set using 'expr-quote' (h: expr-quote), then
" there's nothing that we need to do. If it's set using raw strings, then we
" need to convert it. We need to resort to such voodoo exec magic here to get
" it to work the way we like. '<C-n>' is converted to '\<C-n>' by the end and
" the global vars are replaced by their new value. This is ok since the mapping
" using '<C-n>' should already have completed in the plugin file.
for key in [ 'g:multi_cursor_next_key' ,
\ 'g:multi_cursor_prev_key' ,
\ 'g:multi_cursor_skip_key' ,
\ 'g:multi_cursor_quit_key' ]
if exists ( key )
" Translate raw strings like "<C-n>" into key code like "\<C-n>"
exec 'let temp = ' .key
if temp = ~ '^<.*>$'
exec 'let ' .key .' = "\' .temp .'"'
endif
else
" If the user didn't define it, initialize it to an empty string so the
" logic later don't break
exec 'let ' .key .' = ""'
endif
endfor
" These keys will not be replicated at every cursor location. Make sure that
" this assignment happens AFTER the key tweak setting above
let s :special_keys = {
\ 'v' : [ g :multi_cursor_next_key , g :multi_cursor_prev_key , g :multi_cursor_skip_key ],
\ 'n' : [ g :multi_cursor_next_key ],
\ }
" The highlight group we use for all the cursors
let s :hi_group_cursor = 'multiple_cursors_cursor'
" The highlight group we use for all the visual selection
let s :hi_group_visual = 'multiple_cursors_visual'
" Set up highlighting
if ! hlexists ( s :hi_group_cursor )
exec "highlight " .s :hi_group_cursor ." term=reverse cterm=reverse gui=reverse"
endif
if ! hlexists ( s :hi_group_visual )
exec "highlight link " .s :hi_group_visual ." Visual"
endif
2013-04-14 11:48:31 -04:00
"===============================================================================
" Internal Mappings
"===============================================================================
2013-07-17 19:06:05 -04:00
inoremap < silent > < Plug > ( i ) < C - o > :call < SID > process_user_input ( ) < CR >
nnoremap < silent > < Plug > ( i ) :call < SID > process_user_input ( ) < CR >
xnoremap < silent > < Plug > ( i ) :< C - u > call < SID > process_user_input ( ) < CR >
2013-04-26 12:17:22 -04:00
inoremap < silent > < Plug > ( a ) < C - o > :call < SID > apply_user_input_next ( 'i' ) < CR >
nnoremap < silent > < Plug > ( a ) :call < SID > apply_user_input_next ( 'n' ) < CR >
xnoremap < silent > < Plug > ( a ) :< C - u > call < SID > apply_user_input_next ( 'v' ) < CR >
inoremap < silent > < Plug > ( d ) < C - o > :call < SID > detect_bad_input ( ) < CR >
nnoremap < silent > < Plug > ( d ) :call < SID > detect_bad_input ( ) < CR >
xnoremap < silent > < Plug > ( d ) :< C - u > call < SID > detect_bad_input ( ) < CR >
inoremap < silent > < Plug > ( w ) < C - o > :call < SID > wait_for_user_input ( '' ) < CR >
nnoremap < silent > < Plug > ( w ) :call < SID > wait_for_user_input ( '' ) < CR >
xnoremap < silent > < Plug > ( w ) :< C - u > call < SID > wait_for_user_input ( '' ) < CR >
2013-04-14 11:48:31 -04:00
2013-04-26 12:17:22 -04:00
" Note that although these mappings are seemingly triggerd from Visual mode,
" they are in fact triggered from Normal mode. We quit visual mode to allow the
" virtual highlighting to take over
nnoremap < silent > < Plug > ( p ) :< C - u > call multiple_cursors #prev ( ) < CR >
nnoremap < silent > < Plug > ( s ) :< C - u > call multiple_cursors #skip ( ) < CR >
nnoremap < silent > < Plug > ( n ) :< C - u > call multiple_cursors #new ( 'v' ) < CR >
2013-04-14 11:48:31 -04:00
"===============================================================================
" Public Functions
"===============================================================================
2013-04-26 12:17:22 -04:00
" Print some debugging info
function ! multiple_cursors #debug ( )
call s :cm .debug ( )
2013-04-14 11:48:31 -04:00
endfunction
2013-05-04 16:32:47 -04:00
function ! multiple_cursors #get_latency_debug_file ( )
return s :latency_debug_file
endfunction
2013-04-14 11:48:31 -04:00
" Creates a new cursor. Different logic applies depending on the mode the user
" is in and the current state of the buffer.
" 1. In normal mode, a new cursor is created at the end of the word under Vim's
" normal cursor
" 2. In visual mode, if the visual selection covers more than one line, a new
" cursor is created at the beginning of each line
" 3. In visual mode, if the visual selection covers a single line, a new cursor
" is created at the end of the visual selection. Another cursor will be
" attempted to be created at the next occurrence of the visual selection
function ! multiple_cursors #new ( mode )
if a :mode = = # 'n'
2013-07-17 19:06:05 -04:00
" Reset all existing cursors, don't restore view and setting
call s :cm .reset ( 0 , 0 )
2013-04-14 11:48:31 -04:00
" Select the word under cursor to set the '< and '> marks
2013-04-26 12:17:22 -04:00
exec "normal! viw"
call s :exit_visual_mode ( )
2013-04-14 11:48:31 -04:00
2013-04-26 12:17:22 -04:00
" Add cursor with the current visual selection
call s :cm .add ( s :pos ( "'>" ) , s :region ( "'<" , "'>" ) )
2013-04-14 11:48:31 -04:00
call s :wait_for_user_input ( 'v' )
elseif a :mode = = # 'v'
" If the visual area covers the same line, then do a search for next
" occurrence
let start = line ( "'<" )
let finish = line ( "'>" )
if start ! = finish
2013-07-17 19:06:05 -04:00
call s :cm .reset ( 0 , 0 )
2013-04-26 12:17:22 -04:00
let col = col ( "'<" )
2013-04-14 11:48:31 -04:00
for line in range ( line ( "'<" ) , line ( "'>" ) )
2013-04-26 12:17:22 -04:00
let pos = [line , col ]
call s :cm .add ( pos )
2013-04-14 11:48:31 -04:00
endfor
" Start in normal mode
call s :wait_for_user_input ( 'n' )
else
" Came directly from visual mode
if s :cm .is_empty ( )
2013-07-17 19:06:05 -04:00
call s :cm .reset ( 0 , 0 )
2013-04-26 12:17:22 -04:00
if visualmode ( ) = = # 'V'
let left = [line ( '.' ) , 1 ]
let right = [line ( '.' ) , col ( '$' ) -1 ]
if right [1 ] = = 0 " empty line
return
endif
call s :cm .add ( right , [left , right ])
2013-07-17 19:06:05 -04:00
else
2013-04-26 12:17:22 -04:00
call s :cm .add ( s :pos ( "'>" ) , s :region ( "'<" , "'>" ) )
endif
2013-04-14 11:48:31 -04:00
endif
2013-04-26 12:17:22 -04:00
let content = s :get_text ( s :region ( "'<" , "'>" ) )
let next = s :find_next ( content )
if s :cm .add ( next [1 ], next )
call s :update_visual_markers ( next )
else
call cursor ( s :cm .get_current ( ) .position )
echohl WarningMsg | echo 'No more matches' | echohl None
2013-04-14 11:48:31 -04:00
endif
call s :wait_for_user_input ( 'v' )
endif
endif
endfunction
2013-04-26 12:17:22 -04:00
" Delete the current cursor. If there's no more cursors, stop the loop
2013-04-14 11:48:31 -04:00
function ! multiple_cursors #prev ( )
call s :cm .delete_current ( )
2013-04-26 12:17:22 -04:00
if ! s :cm .is_empty ( )
call s :update_visual_markers ( s :cm .get_current ( ) .visual )
call cursor ( s :cm .get_current ( ) .position )
2013-04-14 11:48:31 -04:00
call s :wait_for_user_input ( 'v' )
endif
endfunction
" Skip the current cursor and move to the next cursor
function ! multiple_cursors #skip ( )
2013-04-26 12:17:22 -04:00
call s :cm .delete_current ( )
let content = s :get_text ( s :region ( "'<" , "'>" ) )
let next = s :find_next ( content )
call s :cm .add ( next [1 ], next )
call s :update_visual_markers ( next )
call s :wait_for_user_input ( 'v' )
endfunction
" Search for pattern between the start and end line number. For each match, add
" a virtual cursor at the end and start multicursor mode
" This function is called from a command. User commands in Vim do not support
" passing in column ranges. If the user selects a block of text in visual mode,
" but not visual line mode, we only want to match patterns within the actual
" visual selection. We get around this by checking the last visual selection and
" see if its start and end lines match the input. If so, we assume that the user
" did a normal visual selection and we use the '< and '> marks to define the
" region instead of start and end from the method parameter.
function ! multiple_cursors #find ( start , end , pattern )
let s :cm .saved_winview = winsaveview ( )
let s :cm .start_from_find = 1
if visualmode ( ) = = # 'v' && a :start = = line ( "'<" ) && a :end = = line ( "'>" )
let pos1 = s :pos ( "'<" )
let pos2 = s :pos ( "'>" )
else
let pos1 = [a :start , 1 ]
let pos2 = [a :end , col ( [a :end , '$' ]) ]
endif
call cursor ( pos1 )
let first = 1
while 1
if first
" First search starts from the current position
let match = search ( a :pattern , 'cW' )
let first = 0
else
let match = search ( a :pattern , 'W' )
endif
if ! match
break
endif
let left = s :pos ( '.' )
call search ( a :pattern , 'ceW' )
let right = s :pos ( '.' )
if s :compare_pos ( right , pos2 ) > 0
break
endif
call s :cm .add ( right , [left , right ])
" Redraw here forces the cursor movement to be updated. This prevents the
" jerky behavior when doing any action once the cursors are added. But it
" also slows down adding the cursors dramatically. We need to a better
" solution here
" redraw
endwhile
2013-04-14 11:48:31 -04:00
if s :cm .is_empty ( )
2013-04-26 12:17:22 -04:00
call winrestview ( s :cm .saved_winview )
echohl ErrorMsg | echo 'No match found' | echohl None
2013-04-14 11:48:31 -04:00
return
2013-07-17 19:06:05 -04:00
else
2013-04-26 12:17:22 -04:00
echohl Normal | echo 'Added ' .s :cm .size ( ) .' cursor' .( s :cm .size ( ) > 1 ?'s' :'' ) | echohl None
call s :wait_for_user_input ( 'v' )
2013-04-14 11:48:31 -04:00
endif
endfunction
"===============================================================================
" Cursor class
"===============================================================================
let s :Cursor = {}
" Create a new cursor. Highlight it and save the current line length
function ! s :Cursor .new ( position )
let obj = copy ( self )
2013-04-26 12:17:22 -04:00
let obj .position = copy ( a :position )
2013-04-14 11:48:31 -04:00
let obj .visual = []
let obj .cursor_hi_id = s :highlight_cursor ( a :position )
let obj .visual_hi_id = 0
2013-04-26 12:17:22 -04:00
let obj .line_length = col ( [a :position [0 ], '$' ])
2013-04-14 11:48:31 -04:00
return obj
endfunction
" Return the line the cursor is on
function ! s :Cursor .line ( ) dict
2013-04-26 12:17:22 -04:00
return self .position [0 ]
2013-04-14 11:48:31 -04:00
endfunction
" Return the column the cursor is on
function ! s :Cursor .column ( ) dict
2013-04-26 12:17:22 -04:00
return self .position [1 ]
2013-04-14 11:48:31 -04:00
endfunction
" Move the cursor location by the number of lines and columns specified in the
" input. The input can be negative.
function ! s :Cursor .move ( line , column ) dict
2013-04-26 12:17:22 -04:00
let self .position [0 ] + = a :line
let self .position [1 ] + = a :column
2013-04-14 11:48:31 -04:00
if ! empty ( self .visual )
2013-04-26 12:17:22 -04:00
let self .visual [0 ][0 ] + = a :line
let self .visual [0 ][1 ] + = a :column
let self .visual [1 ][0 ] + = a :line
let self .visual [1 ][1 ] + = a :column
2013-04-14 11:48:31 -04:00
endif
call self .update_highlight ( )
endfunction
" Update the current position of the cursor
function ! s :Cursor .update_position ( pos ) dict
2013-04-26 12:17:22 -04:00
let self .position [0 ] = a :pos [0 ]
let self .position [1 ] = a :pos [1 ]
2013-04-14 11:48:31 -04:00
call self .update_highlight ( )
endfunction
" Reapply the highlight on the cursor
function ! s :Cursor .update_highlight ( ) dict
call s :cm .remove_highlight ( self .cursor_hi_id )
let self .cursor_hi_id = s :highlight_cursor ( self .position )
endfunction
" Refresh the length of the line the cursor is on. This could change from
" underneath
function ! s :Cursor .update_line_length ( ) dict
let self .line_length = col ( [self .line ( ) , '$' ])
endfunction
" Update the visual selection and its highlight
function ! s :Cursor .update_visual_selection ( region ) dict
2013-04-26 12:17:22 -04:00
let self .visual = deepcopy ( a :region )
2013-04-14 11:48:31 -04:00
call s :cm .remove_highlight ( self .visual_hi_id )
let self .visual_hi_id = s :highlight_region ( a :region )
endfunction
" Remove the visual selection and its highlight
function ! s :Cursor .remove_visual_selection ( ) dict
let self .visual = []
" TODO(terryma): Move functionality into separate class
call s :cm .remove_highlight ( self .visual_hi_id )
let self .visual_hi_id = 0
endfunction
"===============================================================================
" CursorManager class
"===============================================================================
let s :CursorManager = {}
" Constructor
function ! s :CursorManager .new ( )
let obj = copy ( self )
2013-04-26 12:17:22 -04:00
" List of Cursors we're managing
2013-04-14 11:48:31 -04:00
let obj .cursors = []
2013-04-26 12:17:22 -04:00
" Current index into the s:cursors array
2013-04-14 11:48:31 -04:00
let obj .current_index = -1
2013-04-26 12:17:22 -04:00
" This marks the starting cursor index into the s:cursors array
2013-04-14 11:48:31 -04:00
let obj .starting_index = -1
2013-04-26 12:17:22 -04:00
" We save some user settings when the plugin loads initially
2013-04-14 11:48:31 -04:00
let obj .saved_settings = {
\ 'virtualedit' : &virtualedit ,
\ 'cursorline' : &cursorline ,
2013-04-26 12:17:22 -04:00
\ 'lazyredraw' : &lazyredraw ,
2013-07-17 19:06:05 -04:00
\ 'paste' : &paste ,
2013-04-14 11:48:31 -04:00
\ }
2013-04-26 12:17:22 -04:00
" We save the window view when multicursor mode is entered
let obj .saved_winview = []
" Track whether we started multicursor mode from calling multiple_cursors#find
let obj .start_from_find = 0
2013-04-14 11:48:31 -04:00
return obj
endfunction
" Clear all cursors and their highlights
2013-07-17 19:06:05 -04:00
function ! s :CursorManager .reset ( restore_view , restore_setting ) dict
2013-04-26 12:17:22 -04:00
if a :restore_view
" Return the view back to the beginning
if ! empty ( self .saved_winview )
call winrestview ( self .saved_winview )
endif
" If the cursor moved, just restoring the view could get confusing, let's
" put the cursor at where the user left it. Only do this if we didn't start
" from find mode
if ! self .is_empty ( ) && ! self .start_from_find
call cursor ( self .get ( 0 ) .position )
endif
endif
" Delete all cursors and clear their highlights. Don't do clearmatches() as
" that will potentially interfere with other plugins
if ! self .is_empty ( )
for i in range ( self .size ( ) )
call self .remove_highlight ( self .get ( i ) .cursor_hi_id )
call self .remove_highlight ( self .get ( i ) .visual_hi_id )
endfor
endif
2013-04-14 11:48:31 -04:00
let self .cursors = []
let self .current_index = -1
let self .starting_index = -1
2013-04-26 12:17:22 -04:00
let self .saved_winview = []
let self .start_from_find = 0
2013-05-04 16:32:47 -04:00
let s :char = ''
2013-07-17 19:06:05 -04:00
if a :restore_setting
call self .restore_user_settings ( )
endif
2013-04-14 11:48:31 -04:00
endfunction
" Returns 0 if it's not managing any cursors at the moment
function ! s :CursorManager .is_empty ( ) dict
return self .size ( ) = = 0
endfunction
" Returns the number of cursors it's managing
function ! s :CursorManager .size ( ) dict
return len ( self .cursors )
endfunction
" Returns the current cursor
function ! s :CursorManager .get_current ( ) dict
return self .cursors [self .current_index ]
endfunction
" Returns the cursor at index i
function ! s :CursorManager .get ( i ) dict
return self .cursors [a :i ]
endfunction
" Removes the current cursor and all its associated highlighting. Also update
" the current index
function ! s :CursorManager .delete_current ( ) dict
call self .remove_highlight ( self .get_current ( ) .cursor_hi_id )
call self .remove_highlight ( self .get_current ( ) .visual_hi_id )
call remove ( self .cursors , self .current_index )
let self .current_index - = 1
endfunction
2013-04-26 12:17:22 -04:00
" Remove the highlighting if its matchid exists
2013-04-14 11:48:31 -04:00
function ! s :CursorManager .remove_highlight ( hi_id ) dict
if a :hi_id
2013-04-26 12:17:22 -04:00
" If the user did a matchdelete or a clearmatches, we don't want to barf if
" the matchid is no longer valid
silent ! call matchdelete ( a :hi_id )
2013-04-14 11:48:31 -04:00
endif
endfunction
function ! s :CursorManager .debug ( ) dict
let i = 0
for c in self .cursors
2013-04-26 12:17:22 -04:00
echom 'cursor #' .i .': pos=' .string ( c .position ) .' visual=' .string ( c .visual )
2013-04-14 11:48:31 -04:00
let i + = 1
endfor
2013-04-26 12:17:22 -04:00
echom 'input = ' .s :char
echom 'index = ' .self .current_index
echom 'pos = ' .string ( s :pos ( '.' ) )
echom '''< = ' .string ( s :pos ( "'<" ) )
echom '''> = ' .string ( s :pos ( "'>" ) )
echom 'to mode = ' .s :to_mode
echom 'from mode = ' .s :from_mode
" echom 'special keys = '.string(s:special_keys)
2013-04-14 11:48:31 -04:00
echom ' '
endfunction
" Sync the current cursor to the current Vim cursor. This includes updating its
" location, its highlight, and potentially its visual region. Return true if the
" position changed, false otherwise
function ! s :CursorManager .update_current ( ) dict
let cur = self .get_current ( )
2013-04-26 12:17:22 -04:00
if s :to_mode = = # 'v' | | s :to_mode = = # 'V'
" If we're in visual line mode, we need to go to visual mode before we can
" update the visual region
if s :to_mode = = # 'V'
exec "normal! gvv\<Esc>"
endif
" Sets the cursor at the right place
exec "normal! gv\<Esc>"
call cur .update_visual_selection ( s :get_visual_region ( s :pos ( '.' ) ) )
2013-05-04 16:32:47 -04:00
elseif s :from_mode = = # 'v' | | s :from_mode = = # 'V'
2013-04-14 11:48:31 -04:00
call cur .remove_visual_selection ( )
endif
let vdelta = line ( '$' ) - s :saved_linecount
2013-04-26 12:17:22 -04:00
" If the total number of lines changed in the buffer, we need to potentially
" adjust other cursor locations
if vdelta ! = 0
2013-04-14 11:48:31 -04:00
if self .current_index ! = self .size ( ) - 1
let cur_line_length = len ( getline ( cur .line ( ) ) )
let new_line_length = len ( getline ( '.' ) )
for i in range ( self .current_index + 1 , self .size ( ) -1 )
let hdelta = 0
2013-04-26 12:17:22 -04:00
" Note: some versions of Vim don't like chaining function calls like
" a.b().c(). For compatibility reasons, don't do it
let c = self .get ( i )
2013-04-14 11:48:31 -04:00
" If there're other cursors on the same line, we need to adjust their
" columns. This needs to happen before we adjust their line!
2013-04-26 12:17:22 -04:00
if cur .line ( ) = = c .line ( )
2013-04-14 11:48:31 -04:00
if vdelta > 0
" Added a line
let hdelta = cur_line_length * -1
else
" Removed a line
let hdelta = new_line_length
endif
endif
2013-04-26 12:17:22 -04:00
call c .move ( vdelta , hdelta )
2013-04-14 11:48:31 -04:00
endfor
endif
else
" If the line length changes, for all the other cursors on the same line as
" the current one, update their cursor location as well
let hdelta = col ( '$' ) - cur .line_length
" Only do this if we're still on the same line as before
if hdelta ! = 0 && cur .line ( ) = = line ( '.' )
" Update all the cursor's positions that occur after the current cursor on
" the same line
if self .current_index ! = self .size ( ) - 1
for i in range ( self .current_index + 1 , self .size ( ) -1 )
2013-04-26 12:17:22 -04:00
let c = self .get ( i )
2013-04-14 11:48:31 -04:00
" Only do it for cursors on the same line
2013-04-26 12:17:22 -04:00
if cur .line ( ) = = c .line ( )
call c .move ( 0 , hdelta )
2013-04-14 11:48:31 -04:00
else
" Early exit, if we're not on the same line, neither will any cursor
" that come after this
break
endif
endfor
endif
endif
endif
2013-04-26 12:17:22 -04:00
let pos = s :pos ( '.' )
2013-04-14 11:48:31 -04:00
if cur .position = = pos
return 0
endif
call cur .update_position ( pos )
return 1
endfunction
" Advance to the next cursor
function ! s :CursorManager .next ( ) dict
let self .current_index = ( self .current_index + 1 ) % self .size ( )
endfunction
" Start tracking cursor updates
function ! s :CursorManager .start_loop ( ) dict
let self .starting_index = self .current_index
endfunction
" Returns true if we're cycled through all the cursors
function ! s :CursorManager .loop_done ( ) dict
return self .current_index = = self .starting_index
endfunction
2013-04-26 12:17:22 -04:00
" Tweak some user settings, and save our current window view. This is called
" every time multicursor mode is entered.
2013-04-14 11:48:31 -04:00
" virtualedit needs to be set to onemore for updates to work correctly
" cursorline needs to be turned off for the cursor highlight to work on the line
" where the real vim cursor is
2013-04-26 12:17:22 -04:00
" lazyredraw needs to be turned on to prevent jerky screen behavior with many
" cursors on screen
2013-07-17 19:06:05 -04:00
" paste mode needs to be switched off since it turns off a bunch of features
" that's critical for the plugin to function
2013-04-14 11:48:31 -04:00
function ! s :CursorManager .initialize ( ) dict
2013-07-17 19:06:05 -04:00
let self .saved_settings ['virtualedit' ] = &virtualedit
let self .saved_settings ['cursorline' ] = &cursorline
let self .saved_settings ['lazyredraw' ] = &lazyredraw
let self .saved_settings ['paste' ] = &paste
2013-04-14 11:48:31 -04:00
let &virtualedit = "onemore"
let &cursorline = 0
2013-04-26 12:17:22 -04:00
let &lazyredraw = 1
2013-07-17 19:06:05 -04:00
let &paste = 0
2013-04-26 12:17:22 -04:00
" We could have already saved the view from multiple_cursors#find
if ! self .start_from_find
let self .saved_winview = winsaveview ( )
endif
2013-04-14 11:48:31 -04:00
endfunction
" Restore user settings.
function ! s :CursorManager .restore_user_settings ( ) dict
if ! empty ( self .saved_settings )
let &virtualedit = self .saved_settings ['virtualedit' ]
let &cursorline = self .saved_settings ['cursorline' ]
2013-04-26 12:17:22 -04:00
let &lazyredraw = self .saved_settings ['lazyredraw' ]
2013-07-17 19:06:05 -04:00
let &paste = self .saved_settings ['paste' ]
2013-04-14 11:48:31 -04:00
endif
endfunction
" Reselect the current cursor's region in visual mode
function ! s :CursorManager .reapply_visual_selection ( ) dict
call s :select_in_visual_mode ( self .get_current ( ) .visual )
endfunction
2013-04-26 12:17:22 -04:00
" Creates a new virtual cursor as 'pos'
" Optionally a 'region' object can be passed in as second argument. If set, the
" visual region of the cursor will be set to it
" Return true if the cursor has been successfully added, false otherwise
" Mode change: Normal -> Normal
" Cursor change: None (TODO Should we set Vim's cursor to pos?)
function ! s :CursorManager .add ( pos , ...) dict
2013-04-14 11:48:31 -04:00
" Lazy init
if self .is_empty ( )
call self .initialize ( )
endif
" Don't add duplicates
let i = 0
for c in self .cursors
2013-04-26 12:17:22 -04:00
if c .position = = a :pos
2013-04-14 11:48:31 -04:00
return 0
endif
let i + = 1
endfor
2013-04-26 12:17:22 -04:00
let cursor = s :Cursor .new ( a :pos )
2013-04-14 11:48:31 -04:00
" Save the visual selection
2013-04-26 12:17:22 -04:00
if a :0 > 0
call cursor .update_visual_selection ( a :1 )
2013-04-14 11:48:31 -04:00
endif
call add ( self .cursors , cursor )
let self .current_index + = 1
return 1
endfunction
"===============================================================================
" Variables
"===============================================================================
" This is the last user input that we're going to replicate, in its string form
let s :char = ''
" This is the mode the user is in before s:char
2013-04-26 12:17:22 -04:00
let s :from_mode = ''
2013-04-14 11:48:31 -04:00
" This is the mode the user is in after s:char
2013-04-26 12:17:22 -04:00
let s :to_mode = ''
2013-04-14 11:48:31 -04:00
" This is the total number of lines in the buffer before processing s:char
2013-04-26 12:17:22 -04:00
let s :saved_linecount = -1
" This is used to apply the highlight fix. See s:apply_highight_fix()
let s :saved_line = 0
" This is the number of cursor locations where we detected an input that we
" cannot play back
let s :bad_input = 0
2013-04-14 11:48:31 -04:00
" Singleton cursor manager instance
let s :cm = s :CursorManager .new ( )
"===============================================================================
" Utility functions
"===============================================================================
2013-04-26 12:17:22 -04:00
" Return the position of the input marker as a two element array. First element
" is the line number, second element is the column number
function ! s :pos ( mark )
let pos = getpos ( a :mark )
return [pos [1 ], pos [2 ]]
endfunction
" Return the region covered by the input markers as a two element array. First
" element is the position of the start marker, second element is the position of
" the end marker
function ! s :region ( start_mark , end_mark )
return [s :pos ( a :start_mark ) , s :pos ( a :end_mark ) ]
endfunction
2013-04-14 11:48:31 -04:00
" Exit visual mode and go back to normal mode
" The reason for the additional gv\<Esc> is that it allows the cursor to stay
" on where it was before exiting
2013-04-26 12:17:22 -04:00
" Mode change: Normal -> Normal or Visual -> Normal
" Cursor change: If in visual mode, changed to exactly where it was on screen in
" visual mode. If in normal mode, changed to where the cursor was when the last
" visual selection ended
2013-04-14 11:48:31 -04:00
function ! s :exit_visual_mode ( )
exec "normal! \<Esc>gv\<Esc>"
endfunction
" Visually select input region, where region is an array containing the start
" and end position. If start is after end, the selection simply goes backwards.
" Typically m<, m>, and gv would be a simple way of accomplishing this, but on
" some systems, the m< and m> marks are not supported. Note that v`` has random
" behavior if `` is the same location as the cursor location.
2013-04-26 12:17:22 -04:00
" Mode change: Normal -> Visual
" Cursor change: Set to end of region
" TODO: Refactor this and s:update_visual_markers
" FIXME: By using m` we're destroying the user's jumplist. We should use a
" different mark and use :keepjump
2013-04-14 11:48:31 -04:00
function ! s :select_in_visual_mode ( region )
2013-04-26 12:17:22 -04:00
if a :region [0 ] = = a :region [1 ]
2013-04-14 11:48:31 -04:00
normal ! v
2013-04-26 12:17:22 -04:00
else
call cursor ( a :region [1 ])
normal ! m `
call cursor ( a :region [0 ])
2013-04-14 11:48:31 -04:00
normal ! v ``
endif
2013-04-26 12:17:22 -04:00
" Unselect and reselect it again to properly set the '< and '> markers
exec "normal! \<Esc>gv"
endfunction
" Update '< and '> to the input region
" Mode change: Normal -> Normal
" Cursor change: Set to the end of the region
function ! s :update_visual_markers ( region )
if a :region [0 ] = = a :region [1 ]
normal ! v
else
call cursor ( a :region [1 ])
normal ! m `
call cursor ( a :region [0 ])
normal ! v ``
endif
call s :exit_visual_mode ( )
endfunction
" Finds the next occurrence of the input text in the current buffer.
" Search is case sensitive
" Mode change: Normal -> Normal
" Cursor change: Set to the end of the match
function ! s :find_next ( text )
let pattern = '\V\C' .substitute ( escape ( a :text , '\' ) , '\n' , '\\n' , 'g' )
call search ( pattern )
let start = s :pos ( '.' )
call search ( pattern , 'ce' )
let end = s :pos ( '.' )
return [start , end ]
2013-04-14 11:48:31 -04:00
endfunction
" Highlight the position using the cursor highlight group
function ! s :highlight_cursor ( pos )
" Give cursor highlight high priority, to overrule visual selection
2013-04-26 12:17:22 -04:00
return matchadd ( s :hi_group_cursor , '\%' .a :pos [0 ].'l\%' .a :pos [1 ].'c' , 99999 )
2013-04-14 11:48:31 -04:00
endfunction
2013-04-26 12:17:22 -04:00
" Compare two position arrays. Return a negative value if lhs occurs before rhs,
" positive value if after, and 0 if they are the same.
2013-04-14 11:48:31 -04:00
function ! s :compare_pos ( l , r )
" If number lines are the same, compare columns
2013-04-26 12:17:22 -04:00
return a :l [0 ] = = # a :r [0 ] ? a :l [1 ] - a :r [1 ] : a :l [0 ] - a :r [0 ]
2013-04-14 11:48:31 -04:00
endfunction
" Highlight the area bounded by the input region. The logic here really stinks,
" it's frustrating that Vim doesn't have a built in easier way to do this. None
" of the \%V or \%'m solutions work because we need the highlighting to stay for
" multiple places.
function ! s :highlight_region ( region )
let s = sort ( copy ( a :region ) , "s:compare_pos" )
2013-04-26 12:17:22 -04:00
if s :to_mode = = # 'V'
let pattern = '\%>' .( s [0 ][0 ]-1 ) .'l\%<' .( s [1 ][0 ]+ 1 ) .'l.*\ze.\_$'
2013-04-14 11:48:31 -04:00
else
2013-04-26 12:17:22 -04:00
if ( s [0 ][0 ] = = s [1 ][0 ])
" Same line
let pattern = '\%' .s [0 ][0 ].'l\%>' .( s [0 ][1 ]-1 ) .'c.*\%<' .( s [1 ][1 ]+ 1 ) .'c.'
else
" Two lines
let s1 = '\%' .s [0 ][0 ].'l.\%>' .s [0 ][1 ].'c.*'
let s2 = '\%' .s [1 ][0 ].'l.*\%<' .s [1 ][1 ].'c..'
let pattern = s1 .'\|' .s2
" More than two lines
if ( s [1 ][0 ] - s [0 ][0 ] > 1 )
let pattern = pattern .'\|\%>' .s [0 ][0 ].'l\%<' .s [1 ][0 ].'l.*\ze.\_$'
endif
2013-04-14 11:48:31 -04:00
endif
endif
return matchadd ( s :hi_group_visual , pattern )
endfunction
" Perform the operation that's necessary to revert us from one mode to another
function ! s :revert_mode ( from , to )
if a :to = = # 'v'
call s :cm .reapply_visual_selection ( )
endif
2013-04-26 12:17:22 -04:00
if a :to = = # 'V'
call s :cm .reapply_visual_selection ( )
normal ! V
endif
2013-04-14 11:48:31 -04:00
if a :to = = # 'n' && a :from = = # 'i'
stopinsert
endif
endfunction
" Consume all the additional character the user typed between the last
" getchar() and here, to avoid potential race condition.
" TODO(terryma): This solves the problem of cursors getting out of sync, but
" we're potentially losing user input. We COULD replay these characters as
" well...
function ! s :feedkeys ( keys )
while 1
let c = getchar ( 0 )
" Checking type is important, when strings are compared with integers,
" strings are always converted to ints, and all strings are equal to 0
if type ( c ) = = 0 && c = = 0
break
endif
endwhile
call feedkeys ( a :keys )
endfunction
" Take the user input and apply it at every cursor
2013-07-17 19:06:05 -04:00
function ! s :process_user_input ( )
2013-04-14 11:48:31 -04:00
" Grr this is frustrating. In Insert mode, between the feedkey call and here,
" the current position could actually CHANGE for some odd reason. Forcing a
" position reset here
2013-04-26 12:17:22 -04:00
call cursor ( s :cm .get_current ( ) .position )
2013-04-14 11:48:31 -04:00
" Before applying the user input, we need to revert back to the mode the user
" was in when the input was entered
call s :revert_mode ( s :to_mode , s :from_mode )
" Update the line length BEFORE applying any actions. TODO(terryma): Is there
" a better place to do this?
call s :cm .get_current ( ) .update_line_length ( )
let s :saved_linecount = line ( '$' )
" Apply the user input. Note that the above could potentially change mode, we
" use the mapping below to help us determine what the new mode is
2013-04-26 12:17:22 -04:00
" Note that it's possible that \<Plug>(a) never gets called, we have a
" detection mechanism using \<Plug>(d). See its documentation for more details
" Assume that input is not valid
let s :valid_input = 0
" If we're coming from insert mode or going into insert mode, always chain the
" undos together.
" FIXME(terryma): Undo always places the cursor at the beginning of the line.
" Figure out why.
if s :from_mode = = # 'i' | | s :to_mode = = # 'i'
2013-05-04 16:32:47 -04:00
silent ! undojoin | call s :feedkeys ( s :char ."\<Plug>(a)" )
2013-04-26 12:17:22 -04:00
else
2013-05-04 16:32:47 -04:00
call s :feedkeys ( s :char ."\<Plug>(a)" )
2013-04-26 12:17:22 -04:00
endif
" Even when s:char produces invalid input, this method is always called. The
" 't' here is important
call feedkeys ( "\<Plug>(d)" , 't' )
endfunction
" This method is always called during fanout, even when a bad user input causes
" s:apply_user_input_next to not be called. We detect that and force the method
" to be called to continue the fanout process
function ! s :detect_bad_input ( )
if ! s :valid_input
" We ignore the bad input and force invoke s:apply_user_input_next
call feedkeys ( "\<Plug>(a)" )
let s :bad_input + = 1
endif
2013-04-14 11:48:31 -04:00
endfunction
" Apply the user input at the next cursor location
function ! s :apply_user_input_next ( mode )
2013-04-26 12:17:22 -04:00
let s :valid_input = 1
" Save the current mode, only if we haven't already
if empty ( s :to_mode )
let s :to_mode = a :mode
if s :to_mode = = # 'v'
if visualmode ( ) = = # 'V'
let s :to_mode = 'V'
endif
endif
endif
2013-04-14 11:48:31 -04:00
" Update the current cursor's information
let changed = s :cm .update_current ( )
" Advance the cursor index
call s :cm .next ( )
" We're done if we're made the full round
if s :cm .loop_done ( )
2013-04-26 12:17:22 -04:00
if s :to_mode = = # 'v' | | s :to_mode = = # 'V'
" This is necessary to set the "'<" and "'>" markers properly
call s :update_visual_markers ( s :cm .get_current ( ) .visual )
2013-04-14 11:48:31 -04:00
endif
2013-04-26 12:17:22 -04:00
call feedkeys ( "\<Plug>(w)" )
2013-04-14 11:48:31 -04:00
else
" Continue to next
2013-04-26 12:17:22 -04:00
call feedkeys ( "\<Plug>(i)" )
2013-04-14 11:48:31 -04:00
endif
endfunction
2013-04-26 12:17:22 -04:00
" If pos is equal to the left side of the visual selection, the region start
" from end to start
function ! s :get_visual_region ( pos )
let left = s :pos ( "'<" )
let right = s :pos ( "'>" )
if a :pos = = left
2013-04-14 11:48:31 -04:00
let region = [right , left ]
else
let region = [left , right ]
endif
return region
endfunction
2013-04-26 12:17:22 -04:00
" Return the content of the buffer between the input region. This is used to
" find the next match in the buffer
" Mode change: Normal -> Normal
" Cursor change: None
function ! s :get_text ( region )
let lines = getline ( a :region [0 ][0 ], a :region [1 ][0 ])
let lines [-1 ] = lines [-1 ][:a :region [1 ][1 ] - 1 ]
let lines [0 ] = lines [0 ][a :region [0 ][1 ] - 1 :]
2013-04-14 11:48:31 -04:00
return join ( lines , "\n" )
endfunction
" Wrapper around getchar() that returns the string representation of the user
" input
function ! s :get_char ( )
let c = getchar ( )
" If the character is a number, then it's not a special key
if type ( c ) = = 0
let c = nr2char ( c )
endif
return c
endfunction
" Quits multicursor mode and clears all cursors. Return true if exited
" successfully.
function ! s :exit ( )
2013-04-26 12:17:22 -04:00
if s :char ! = # g :multi_cursor_quit_key
return 0
endif
let exit = 0
if s :from_mode = = # 'n'
let exit = 1
elseif ( s :from_mode = = # 'v' | | s :from_mode = = # 'V' ) &&
\ g :multi_cursor_exit_from_visual_mode
let exit = 1
elseif s :from_mode = = # 'i' && g :multi_cursor_exit_from_insert_mode
stopinsert
let exit = 1
endif
if exit
2013-07-17 19:06:05 -04:00
call s :cm .reset ( 1 , 1 )
2013-04-14 11:48:31 -04:00
return 1
endif
return 0
endfunction
2013-04-26 12:17:22 -04:00
" These keys don't get faned out to all cursor locations. Instead, they're used
" to add new / remove existing cursors
" Precondition: The function is only called when the keys and mode respect the
" setting in s:special_keys
function ! s :handle_special_key ( key , mode )
" Use feedkeys here instead of calling the function directly to prevent
" increasing the call stack, since feedkeys execute after the current call
" finishes
if a :key = = g :multi_cursor_next_key
call s :feedkeys ( "\<Plug>(n)" )
elseif a :key = = g :multi_cursor_prev_key
call s :feedkeys ( "\<Plug>(p)" )
elseif a :key = = g :multi_cursor_skip_key
call s :feedkeys ( "\<Plug>(s)" )
endif
endfunction
" The last line where the normal Vim cursor is always seems to highlighting
" issues if the cursor is on the last column. Vim's cursor seems to override the
" highlight of the virtual cursor. This won't happen if the virtual cursor isn't
" the last character on the line. This is a hack to add an empty space on the
" Vim cursor line right before we do the redraw, we'll revert the change
" immedidately after the redraw so the change should not be intrusive to the
" user's buffer content
function ! s :apply_highlight_fix ( )
" Only do this if we're on the last character of the line
if col ( '.' ) = = col ( '$' )
let s :saved_line = getline ( '.' )
if s :from_mode = = # 'i'
silent ! undojoin | call setline ( '.' , s :saved_line .' ' )
else
call setline ( '.' , s :saved_line .' ' )
endif
endif
endfunction
" Revert the fix if it was applied earlier
function ! s :revert_highlight_fix ( )
if type ( s :saved_line ) = = 1
if s :from_mode = = # 'i'
silent ! undojoin | call setline ( '.' , s :saved_line )
2013-07-17 19:06:05 -04:00
else
2013-04-26 12:17:22 -04:00
call setline ( '.' , s :saved_line )
endif
endif
let s :saved_line = 0
endfunction
function ! s :display_error ( )
if s :bad_input > 0
echohl ErrorMsg |
\ echo "Key '" .s :char ."' cannot be replayed at " .
\ s :bad_input ." cursor location" .( s :bad_input = = 1 ? '' : 's' ) |
\ echohl Normal
endif
let s :bad_input = 0
endfunction
2013-05-04 16:32:47 -04:00
let s :latency_debug_file = ''
function ! s :start_latency_measure ( )
if g :multi_cursor_debug_latency
let s :start_time = reltime ( )
endif
endfunction
function ! s :skip_latency_measure ( )
if g :multi_cursor_debug_latency
let s :skip_latency_measure = 1
endif
endfunction
function ! s :end_latency_measure ( )
if g :multi_cursor_debug_latency && ! empty ( s :char )
if empty ( s :latency_debug_file )
let s :latency_debug_file = tempname ( )
exec 'redir >> ' .s :latency_debug_file
silent ! echom "Starting latency debug at " .reltimestr ( reltime ( ) )
redir END
endif
2013-07-17 19:06:05 -04:00
2013-05-04 16:32:47 -04:00
if ! s :skip_latency_measure
exec 'redir >> ' .s :latency_debug_file
silent ! echom "Processing '" .s :char ."' took " .string ( str2float ( reltimestr ( reltime ( s :start_time ) ) ) *1000 ) .' ms in ' .s :cm .size ( ) .' cursors. mode = ' .s :from_mode
redir END
endif
endif
let s :skip_latency_measure = 0
endfunction
2013-04-14 11:48:31 -04:00
function ! s :wait_for_user_input ( mode )
let s :from_mode = a :mode
2013-04-26 12:17:22 -04:00
if empty ( a :mode )
let s :from_mode = s :to_mode
endif
2013-04-14 11:48:31 -04:00
let s :to_mode = ''
2013-04-26 12:17:22 -04:00
call s :display_error ( )
" Right before redraw, apply the highlighting bug fix
call s :apply_highlight_fix ( )
2013-04-14 11:48:31 -04:00
redraw
2013-04-26 12:17:22 -04:00
" Immediately revert the change to leave the user's buffer unchanged
call s :revert_highlight_fix ( )
2013-05-04 16:32:47 -04:00
call s :end_latency_measure ( )
2013-04-14 11:48:31 -04:00
let s :char = s :get_char ( )
2013-04-26 12:17:22 -04:00
2013-05-04 16:32:47 -04:00
call s :start_latency_measure ( )
2013-04-26 12:17:22 -04:00
" Clears any echoes we might've added
normal ! :< Esc >
2013-04-14 11:48:31 -04:00
if s :exit ( )
return
endif
2013-07-17 19:06:05 -04:00
2013-04-26 12:17:22 -04:00
" If the key is a special key and we're in the right mode, handle it
if index ( get ( s :special_keys , s :from_mode , []) , s :char ) ! = -1
call s :handle_special_key ( s :char , s :from_mode )
2013-05-04 16:32:47 -04:00
call s :skip_latency_measure ( )
2013-04-14 11:48:31 -04:00
else
call s :cm .start_loop ( )
2013-04-26 12:17:22 -04:00
call s :feedkeys ( "\<Plug>(i)" )
2013-04-14 11:48:31 -04:00
endif
endfunction