"=============================================================================== " 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. '' is converted to '\' by the end and " the global vars are replaced by their new value. This is ok since the mapping " using '' should already have completed in the plugin file. for s:key in [ 'g:multi_cursor_next_key', \ 'g:multi_cursor_prev_key', \ 'g:multi_cursor_skip_key', \ 'g:multi_cursor_quit_key' ] if exists(s:key) " Translate raw strings like "" into key code like "\" exec 'let s:temp = '.s:key if s:temp =~ '^<.*>$' exec 'let '.s:key.' = "\'.s: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 '.s:key.' = ""' endif endfor unlet! s:key s:temp " 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' " Used for preventing multiple calls on before function let s:before_function_called = 0 " Used for searching whole words (search pattern is wrapped with \< and \>) " Keep old behaviour by default (act like g*) let s:use_word_boundary = 1 " 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 " Temporary buffer that is used for individual paste buffer save/restore " operations let s:paste_buffer_temporary_text = '' let s:paste_buffer_temporary_type = '' "=============================================================================== " Internal Mappings "=============================================================================== inoremap (multiple-cursors-input) :call process_user_input() nnoremap (multiple-cursors-input) :call process_user_input() xnoremap (multiple-cursors-input) :call process_user_input() inoremap (multiple-cursors-apply) :call apply_user_input_next('i') nnoremap (multiple-cursors-apply) :call apply_user_input_next('n') xnoremap (multiple-cursors-apply) :call apply_user_input_next('v') inoremap (multiple-cursors-detect) :call detect_bad_input() nnoremap (multiple-cursors-detect) :call detect_bad_input() xnoremap (multiple-cursors-detect) :call detect_bad_input() inoremap (multiple-cursors-wait) :call wait_for_user_input('') nnoremap (multiple-cursors-wait) :call wait_for_user_input('') xnoremap (multiple-cursors-wait) :call wait_for_user_input('') " 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 (multiple-cursors-prev) :call multiple_cursors#prev() nnoremap (multiple-cursors-skip) :call multiple_cursors#skip() nnoremap (multiple-cursors-new) :call multiple_cursors#new('v', 0) nnoremap (multiple-cursors-new-word) :call multiple_cursors#new('v', 1) "=============================================================================== " Public Functions "=============================================================================== " Print some debugging info function! multiple_cursors#debug() call s:cm.debug() endfunction function! multiple_cursors#get_latency_debug_file() return s:latency_debug_file endfunction " 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, word_boundary) " Call before function if exists only once until it is canceled () if exists('*Multiple_cursors_before') && !s:before_function_called exe "call Multiple_cursors_before()" let s:before_function_called = 1 endif let s:use_word_boundary = a:word_boundary if a:mode ==# 'n' " Reset all existing cursors, don't restore view and setting call s:cm.reset(0, 0) " Select the word under cursor to set the '< and '> marks exec "normal! viw" call s:exit_visual_mode() " Add cursor with the current visual selection call s:cm.add(s:pos("'>"), s:region("'<", "'>")) 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 call s:cm.reset(0, 0) let col = col("'<") for line in range(line("'<"), line("'>")) let pos = [line, col] call s:cm.add(pos) endfor " Start in normal mode call s:wait_for_user_input('n') else " Came directly from visual mode if s:cm.is_empty() call s:cm.reset(0, 0) 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]) else call s:cm.add(s:pos("'>"), s:region("'<", "'>")) endif endif 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 endif call s:wait_for_user_input('v') endif endif endfunction " Quit out of multicursor mode, fixes #27. function! multiple_cursors#quit() call s:exit() endfunction " Delete the current cursor. If there's no more cursors, stop the loop function! multiple_cursors#prev() call s:cm.delete_current() if !s:cm.is_empty() call s:update_visual_markers(s:cm.get_current().visual) call cursor(s:cm.get_current().position) call s:wait_for_user_input('v') endif endfunction " Skip the current cursor and move to the next cursor function! multiple_cursors#skip() 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 if s:cm.is_empty() call winrestview(s:cm.saved_winview) echohl ErrorMsg | echo 'No match found' | echohl None return else echohl Normal | echo 'Added '.s:cm.size().' cursor'.(s:cm.size()>1?'s':'') | echohl None " If we've created any cursors, we need to call the before function, end " function will be called via normal routes if exists('*Multiple_cursors_before') && !s:before_function_called exe "call Multiple_cursors_before()" let s:before_function_called = 1 endif call s:wait_for_user_input('v') 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) let obj.position = copy(a:position) let obj.visual = [] let obj.saved_visual = [] " Stores text that was yanked after any commands in Normal or Visual mode let obj.paste_buffer_text = getreg('"') let obj.paste_buffer_type = getregtype('"') let obj.cursor_hi_id = s:highlight_cursor(a:position) let obj.visual_hi_id = 0 let obj.line_length = col([a:position[0], '$']) if has('folding') silent! execute a:position[0] . "foldopen!" endif return obj endfunction " Return the line the cursor is on function! s:Cursor.line() dict return self.position[0] endfunction " Return the column the cursor is on function! s:Cursor.column() dict return self.position[1] 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 let self.position[0] += a:line let self.position[1] += a:column if !empty(self.visual) 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 endif call self.update_highlight() endfunction " Update the current position of the cursor function! s:Cursor.update_position(pos) dict let self.position[0] = a:pos[0] let self.position[1] = a:pos[1] 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 let self.visual = deepcopy(a:region) 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.saved_visual = deepcopy(self.visual) 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 " Restore unnamed register from paste buffer function! s:Cursor.restore_unnamed_register() dict call setreg('"', self.paste_buffer_text, self.paste_buffer_type) endfunction " Save contents of the unnamed register into paste buffer function! s:Cursor.save_unnamed_register() dict let self.paste_buffer_text = getreg('"') let self.paste_buffer_type = getregtype('"') endfunction "=============================================================================== " CursorManager class "=============================================================================== let s:CursorManager = {} " Constructor function! s:CursorManager.new() let obj = copy(self) " List of Cursors we're managing let obj.cursors = [] " Current index into the s:cursors array let obj.current_index = -1 " This marks the starting cursor index into the s:cursors array let obj.starting_index = -1 " We save some user settings when the plugin loads initially let obj.saved_settings = { \ 'virtualedit': &virtualedit, \ 'cursorline': &cursorline, \ 'lazyredraw': &lazyredraw, \ 'paste': &paste, \ 'clipboard': &clipboard, \ } " 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 return obj endfunction " Clear all cursors and their highlights function! s:CursorManager.reset(restore_view, restore_setting, ...) dict 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 let self.cursors = [] let self.current_index = -1 let self.starting_index = -1 let self.saved_winview = [] let self.start_from_find = 0 let s:char = '' let s:saved_char = '' if a:restore_setting call self.restore_user_settings() endif " Call after function if exists and only if action is canceled () if exists('*Multiple_cursors_after') && a:0 && s:before_function_called exe "call Multiple_cursors_after()" let s:before_function_called = 0 endif 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 " Remove the highlighting if its matchid exists function! s:CursorManager.remove_highlight(hi_id) dict if a:hi_id " 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) endif endfunction function! s:CursorManager.debug() dict let i = 0 for c in self.cursors echom 'cursor #'.i.': pos='.string(c.position).' visual='.string(c.visual) let i+=1 endfor 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) 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() 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\" endif " Sets the cursor at the right place exec "normal! gv\" call cur.update_visual_selection(s:get_visual_region(s:pos('.'))) elseif s:from_mode ==# 'v' || s:from_mode ==# 'V' " Save contents of unnamed register after each operation in Visual mode. " This should be executed after user input is processed, when unnamed " register already contains the text. call cur.save_unnamed_register() call cur.remove_visual_selection() elseif s:from_mode ==# 'i' && s:to_mode ==# 'n' && self.current_index != self.size() - 1 normal! h elseif s:from_mode ==# 'n' " Save contents of unnamed register after each operation in Normal mode. call cur.save_unnamed_register() endif let vdelta = line('$') - s:saved_linecount " If the total number of lines changed in the buffer, we need to potentially " adjust other cursor locations if vdelta != 0 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 " 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) " If there're other cursors on the same line, we need to adjust their " columns. This needs to happen before we adjust their line! if cur.line() == c.line() if vdelta > 0 " Added a line let hdelta = cur_line_length * -1 else " Removed a line let hdelta = new_line_length endif endif call c.move(vdelta, hdelta) 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) let c = self.get(i) " Only do it for cursors on the same line if cur.line() == c.line() call c.move(0, hdelta) 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 let pos = s:pos('.') 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 " Tweak some user settings, and save our current window view. This is called " every time multicursor mode is entered. " 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 " lazyredraw needs to be turned on to prevent jerky screen behavior with many " cursors on screen " paste mode needs to be switched off since it turns off a bunch of features " that's critical for the plugin to function " clipboard should not have unnamed and unnamedplus otherwise plugin cannot " reliably use unnamed register ('"') function! s:CursorManager.initialize() dict let self.saved_settings['virtualedit'] = &virtualedit let self.saved_settings['cursorline'] = &cursorline let self.saved_settings['lazyredraw'] = &lazyredraw let self.saved_settings['paste'] = &paste let self.saved_settings['clipboard'] = &clipboard let &virtualedit = "onemore" let &cursorline = 0 let &lazyredraw = 1 let &paste = 0 set clipboard-=unnamed clipboard-=unnamedplus " We could have already saved the view from multiple_cursors#find if !self.start_from_find let self.saved_winview = winsaveview() endif " Save contents and type of unnamed register upon entering multicursor mode " to restore it later when leaving mode let s:paste_buffer_temporary_text = getreg('"') let s:paste_buffer_temporary_type = getregtype('"') 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'] let &lazyredraw = self.saved_settings['lazyredraw'] let &paste = self.saved_settings['paste'] let &clipboard = self.saved_settings['clipboard'] endif " Restore original contents and type of unnamed register. This method is " called from reset, which calls us only when restore_setting argument is " true, which happens only when we leave multicursor mode. This should be " symmetrical to saving of unnamed register upon the start of multicursor " mode. call setreg('"', s:paste_buffer_temporary_text, s:paste_buffer_temporary_type) endfunction " Reposition all cursors to the start or end of their region function! s:CursorManager.reposition_all_within_region(start) dict for c in self.cursors call c.update_position(c.saved_visual[a:start ? 0 : 1]) endfor 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 " 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 " Lazy init if self.is_empty() call self.initialize() endif " Don't add duplicates let i = 0 for c in self.cursors if c.position == a:pos return 0 endif let i+=1 endfor let cursor = s:Cursor.new(a:pos) " Save the visual selection if a:0 > 0 call cursor.update_visual_selection(a:1) 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 either `I` or `A`, as input in Visual mode, that we're going to use " to make the appropriate transition into Insert mode let s:saved_char = '' " This is the mode the user is in before s:char let s:from_mode = '' " This is the mode the user is in after s:char let s:to_mode = '' " This is the total number of lines in the buffer before processing s:char 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 " Singleton cursor manager instance let s:cm = s:CursorManager.new() "=============================================================================== " Utility functions "=============================================================================== " 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 " Exit visual mode and go back to normal mode " The reason for the additional gv\ is that it allows the cursor to stay " on where it was before exiting " 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 function! s:exit_visual_mode() exec "normal! \gv\" " Call before function if exists only once until it is canceled () if exists('*Multiple_cursors_before') && !s:before_function_called exe "call Multiple_cursors_before()" let s:before_function_called = 1 endif 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. " 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 function! s:select_in_visual_mode(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 " Unselect and reselect it again to properly set the '< and '> markers exec "normal! \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 = substitute(escape(a:text, '\'), '\n', '\\n', 'g') if s:use_word_boundary == 1 let pattern = '\<'.pattern.'\>' endif let pattern = '\V\C'.pattern call search(pattern) let start = s:pos('.') call search(pattern, 'ce') let end = s:pos('.') return [start, end] endfunction " Highlight the position using the cursor highlight group function! s:highlight_cursor(pos) " Give cursor highlight high priority, to overrule visual selection return matchadd(s:hi_group_cursor, '\%'.a:pos[0].'l\%'.a:pos[1].'c', 99999) endfunction " Compare two position arrays. Return a negative value if lhs occurs before rhs, " positive value if after, and 0 if they are the same. function! s:compare_pos(l, r) " If number lines are the same, compare columns return a:l[0] ==# a:r[0] ? a:l[1] - a:r[1] : a:l[0] - a:r[0] 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") if s:to_mode ==# 'V' let pattern = '\%>'.(s[0][0]-1).'l\%<'.(s[1][0]+1).'l.*\ze.\_$' else 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 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() elseif a:to ==# 'V' call s:cm.reapply_visual_selection() normal! V elseif 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. let s:saved_keys = "" function! s:feedkeys(keys) while 1 let c = getchar(0) let char_type = type(c) " Checking type is important, when strings are compared with integers, " strings are always converted to ints, and all strings are equal to 0 if char_type == 0 if c == 0 break else let s:saved_keys .= nr2char(c) endif elseif char_type == 1 " char with more than 8 bits (as string) let s:saved_keys .= c endif endwhile call feedkeys(a:keys) endfunction " Take the user input and apply it at every cursor function! s:process_user_input() " 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 call cursor(s:cm.get_current().position) " 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('$') " Restore unnamed register only in Normal mode. This should happen before user " input is processed. if s:from_mode ==# 'n' || s:from_mode ==# 'v' || s:from_mode ==# 'V' call s:cm.get_current().restore_unnamed_register() endif " 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 " Note that it's possible that \(multiple-cursors-apply) never gets called, we have a " detection mechanism using \(multiple-cursors-detect). 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' silent! undojoin | call s:feedkeys(s:char."\(multiple-cursors-apply)") else call s:feedkeys(s:char."\(multiple-cursors-apply)") endif " Even when s:char produces invalid input, this method is always called. The " 't' here is important call feedkeys("\(multiple-cursors-detect)", '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 " To invoke the appropriate `(multiple-cursors-apply)` mapping, 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) " We ignore the bad input and force invoke s:apply_user_input_next call feedkeys("\(multiple-cursors-apply)") let s:bad_input += 1 endif endfunction " Complete transition into Insert mode when `I` or `A` is input in Visual mode function! s:handle_visual_IA_to_insert() if !empty(s:saved_char) && s:char =~# 'v\|V' && s:to_mode ==# 'n' if s:saved_char ==# 'I' call s:cm.reposition_all_within_region(1) endif call feedkeys(tolower(s:saved_char)) let s:saved_char = '' endif endfunction " Begin transition into Insert mode when `I` or `A` is input in Visual mode function! s:handle_visual_IA_to_normal() if s:char =~# 'I\|A' && s:from_mode =~# 'v\|V' let s:saved_char = s:char let s:char = s:from_mode " spoof a 'v' or 'V' input to transiton from Visual into Normal mode endif endfunction " Apply the user input at the next cursor location function! s:apply_user_input_next(mode) 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 " 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() 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) endif call feedkeys("\(multiple-cursors-wait)") call s:handle_visual_IA_to_insert() else " Continue to next call feedkeys("\(multiple-cursors-input)") endif endfunction " 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 let region = [right, left] else let region = [left, right] endif return region endfunction " 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:] 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() if s:last_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 call s:cm.reset(1, 1, 1) return 1 endif return 0 endfunction " 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 if s:use_word_boundary == 1 call s:feedkeys("\(multiple-cursors-new-word)") else call s:feedkeys("\(multiple-cursors-new)") endif elseif a:key == g:multi_cursor_prev_key call s:feedkeys("\(multiple-cursors-prev)") elseif a:key == g:multi_cursor_skip_key call s:feedkeys("\(multiple-cursors-skip)") 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) else call setline('.', s:saved_line) endif endif let s:saved_line = 0 endfunction let s:retry_keys = "" function! s:display_error() if s:bad_input == s:cm.size() \ && ((s:from_mode ==# 'n' && has_key(g:multi_cursor_normal_maps, s:char[0])) \ || (s:from_mode =~# 'v\|V' && has_key(g:multi_cursor_visual_maps, s:char[0]))) " we couldn't replay it anywhere but we're told it's the beginning of a " multi-character map like the `d` in `dw` let s:retry_keys = s:char else let s:retry_keys = "" 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 endif let s:bad_input = 0 endfunction 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 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 function! s:last_char() return s:char[len(s:char)-1] endfunction function! s:wait_for_user_input(mode) call s:display_error() let s:from_mode = a:mode if empty(a:mode) let s:from_mode = s:to_mode endif let s:to_mode = '' " Right before redraw, apply the highlighting bug fix call s:apply_highlight_fix() redraw " Immediately revert the change to leave the user's buffer unchanged call s:revert_highlight_fix() call s:end_latency_measure() let s:char = s:retry_keys . s:saved_keys if len(s:saved_keys) == 0 let s:char .= s:get_char() call s:handle_visual_IA_to_normal() else let s:saved_keys = "" endif if s:from_mode ==# 'i' && has_key(g:multi_cursor_insert_maps, s:last_char()) let c = getchar(0) let char_type = type(c) let poll_count = 0 while char_type == 0 && c == 0 && poll_count < &timeoutlen sleep 1m let c = getchar(0) let char_type = type(c) let poll_count += 1 endwhile if char_type == 0 && c != 0 let s:char .= nr2char(c) elseif char_type == 1 " char with more than 8 bits (as string) let s:char .= c endif elseif s:from_mode !=# 'i' && s:char[0] ==# ":" call feedkeys(s:char) call s:cm.reset(1, 1) return elseif s:from_mode ==# 'n' while match(s:last_char(), "\\d") == 0 let s:char .= s:get_char() endwhile endif call s:start_latency_measure() " Clears any echoes we might've added normal! : if s:exit() return endif " 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:last_char()) != -1 call s:handle_special_key(s:last_char(), s:from_mode) call s:skip_latency_measure() else call s:cm.start_loop() call s:feedkeys("\(multiple-cursors-input)") endif endfunction