tessl install tessl/npm-coc-nvim@0.0.0LSP based intellisense engine for neovim & vim8.
Complexity: Intermediate | Category: Advanced | Keywords: multi-cursor, cursors, selection, editing
Common Tasks: Select word occurrences | Add cursors manually | Edit multiple locations | Range selection
Multiple cursor support for simultaneous editing at multiple locations.
nmap <silent> <C-d> <Plug>(coc-cursors-word)Press repeatedly to add more matches.
nmap <silent> <C-c> <Plug>(coc-cursors-position)Manually place cursors where needed.
xmap <silent> <C-c> <Plug>(coc-cursors-range)Add multiple cursor ranges from selection.
if get(b:, 'coc_cursors_activated', 0)
echo "Multi-cursor mode active"
endifQuery buffer state for multi-cursor mode.
<Plug>(coc-cursors-operator)Add cursors via operator motion.
Example:
nmap <silent> <C-c> <Plug>(coc-cursors-operator)
" Then use with motions:
" <C-c>iw - add cursor to word
" <C-c>ap - add cursor to paragraph<Plug>(coc-cursors-word)Add cursor to word under cursor. Press multiple times to add more occurrences.
Example:
nmap <expr> <silent> <C-d> <SID>select_current_word()
function! s:select_current_word()
if !get(b:, 'coc_cursors_activated', 0)
return "\<Plug>(coc-cursors-word)"
endif
return "\<Plug>(coc-cursors-word)"
endfunction<Plug>(coc-cursors-position)Add cursor at current position.
Example:
nmap <silent> <C-c>p <Plug>(coc-cursors-position)<Plug>(coc-cursors-range)Add cursor range in visual mode.
Example:
xmap <silent> <C-c> <Plug>(coc-cursors-range)CocAction('cursorsSelect', bufnr, mode, type)Select cursor ranges programmatically.
Parameters:
bufnr: Buffer numbermode: Selection mode ('n', 'v', 'V', etc.)type: Selection type ('word', 'position', 'range')Example:
call CocAction('cursorsSelect', bufnr('%'), 'n', 'word')CocAction('addRanges', ranges)Add multiple cursor ranges.
Parameters:
ranges: List of range objects with start and end LSP positionsExample:
let ranges = [
\ {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 5}},
\ {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 5}}
\ ]
call CocAction('addRanges', ranges)b:coc_cursors_activatedIndicates if multi-cursor mode is active in buffer.
Type: Number (read-only)
Values: 1 when active, 0 otherwise
Example:
if get(b:, 'coc_cursors_activated', 0)
echo "Multi-cursor mode is active"
endifg:coc_selected_textLast selected text in multi-cursor mode.
Type: String (read-only)
Example:
if exists('g:coc_selected_text')
echo 'Selected: ' . g:coc_selected_text
endifMulti-cursor settings in coc-settings.json:
{
"cursors.cancelKey": "<esc>",
"cursors.nextKey": "<C-n>",
"cursors.previousKey": "<C-p>"
}CocCursorRangeHighlight for cursor ranges.
Example:
highlight CocCursorRange ctermbg=237 guibg=#3a3a3a" Select word and add more occurrences
nmap <silent> <C-d> <Plug>(coc-cursors-word)
" Add cursor at position
nmap <silent> <C-c> <Plug>(coc-cursors-position)
" Visual range
xmap <silent> <C-c> <Plug>(coc-cursors-range)
" Operator mode
nmap <expr> <silent> <C-c> <SID>coc_cursors_operator()
function! s:coc_cursors_operator()
return "\<Plug>(coc-cursors-operator)"
endfunction" Ctrl-D to select word and add next occurrence
nmap <expr> <silent> <C-d> <SID>select_current_word()
function! s:select_current_word()
if !get(b:, 'coc_cursors_activated', 0)
return "\<Plug>(coc-cursors-word)"
endif
return "\<Plug>(coc-cursors-word)*N"
endfunction
" Alt-Click to add cursor (if terminal supports it)
nmap <M-LeftMouse> <Plug>(coc-cursors-position)function! MultiCursorStatus() abort
if get(b:, 'coc_cursors_activated', 0)
return '[MULTI]'
endif
return ''
endfunction
set statusline+=%{MultiCursorStatus()}" Press Escape to exit multi-cursor mode
function! ExitCursorMode() abort
if get(b:, 'coc_cursors_activated', 0)
call coc#rpc#notify('cursorsCancel', [])
return "\<Esc>"
endif
return "\<Esc>"
endfunction
nnoremap <silent><expr> <Esc> ExitCursorMode()function! SelectAllOccurrences() abort
" Get word under cursor
let word = expand('<cword>')
" Get all occurrences
let pattern = '\<' . word . '\>'
let matches = []
let lnum = 1
while lnum <= line('$')
let line = getline(lnum)
let col = 0
while 1
let col = match(line, pattern, col)
if col == -1
break
endif
call add(matches, {
\ 'start': {'line': lnum - 1, 'character': col},
\ 'end': {'line': lnum - 1, 'character': col + len(word)}
\ })
let col += 1
endwhile
let lnum += 1
endwhile
if len(matches) > 0
call CocAction('addRanges', matches)
echo 'Selected ' . len(matches) . ' occurrences'
else
echo 'No matches found'
endif
endfunction
nnoremap <silent> <leader>ma :call SelectAllOccurrences()<CR>function! AddCursorAbove() abort
let line = line('.') - 1
if line > 0
call cursor(line, col('.'))
call CocAction('cursorsSelect', bufnr('%'), 'n', 'position')
endif
endfunction
function! AddCursorBelow() abort
let line = line('.') + 1
if line <= line('$')
call cursor(line, col('.'))
call CocAction('cursorsSelect', bufnr('%'), 'n', 'position')
endif
endfunction
nnoremap <silent> <C-S-Up> :call AddCursorAbove()<CR>
nnoremap <silent> <C-S-Down> :call AddCursorBelow()<CR>function! SelectWordInRange(start, end) abort
let word = expand('<cword>')
let pattern = '\<' . word . '\>'
let matches = []
for lnum in range(a:start, a:end)
let line = getline(lnum)
let col = 0
while 1
let col = match(line, pattern, col)
if col == -1
break
endif
call add(matches, {
\ 'start': {'line': lnum - 1, 'character': col},
\ 'end': {'line': lnum - 1, 'character': col + len(word)}
\ })
let col += 1
endwhile
endfor
call CocAction('addRanges', matches)
echo 'Added ' . len(matches) . ' cursors'
endfunction
command! -range SelectInRange call SelectWordInRange(<line1>, <line2>)function! AddMultipleCursors(count) abort
for i in range(a:count)
execute "normal \<Plug>(coc-cursors-word)"
endfor
echo 'Added ' . a:count . ' cursors'
endfunction
" Usage: 3<C-d> to add 3 occurrences
nnoremap <silent> <C-d> :<C-u>call AddMultipleCursors(v:count1)<CR>" Skip current match and select next (like Ctrl-K in VSCode)
function! SkipCurrentMatch() abort
if !get(b:, 'coc_cursors_activated', 0)
return
endif
" Move to next match without selecting current
call coc#rpc#notify('cursorsNext', [])
endfunction
nmap <silent> <C-k> :call SkipCurrentMatch()<CR>" Create cursors from visual selection
function! CursorsFromVisual() abort
let [line_start, column_start] = getpos("'<")[1:2]
let [line_end, column_end] = getpos("'>")[1:2]
if line_start == line_end
" Single line selection
call CocAction('cursorsSelect', bufnr('%'), 'v', 'range')
else
" Multi-line: create cursor at same column on each line
let ranges = []
for lnum in range(line_start, line_end)
call add(ranges, {
\ 'start': {'line': lnum - 1, 'character': column_start - 1},
\ 'end': {'line': lnum - 1, 'character': column_start - 1}
\ })
endfor
call CocAction('addRanges', ranges)
endif
endfunction
xnoremap <silent> <C-n> :<C-u>call CursorsFromVisual()<CR>function! CountCursors() abort
if !get(b:, 'coc_cursors_activated', 0)
echo "No multi-cursors active"
return
endif
" This is a placeholder - actual count needs to be tracked
echo "Multi-cursor mode active"
endfunction
command! CountCursors call CountCursors()function! AddCursorsFromRegex(pattern) abort
let matches = []
let lnum = 1
while lnum <= line('$')
let line = getline(lnum)
let col = 0
while 1
let match = matchstrpos(line, a:pattern, col)
if match[1] == -1
break
endif
call add(matches, {
\ 'start': {'line': lnum - 1, 'character': match[1]},
\ 'end': {'line': lnum - 1, 'character': match[2]}
\ })
let col = match[2]
endwhile
let lnum += 1
endwhile
if len(matches) > 0
call CocAction('addRanges', matches)
echo 'Added ' . len(matches) . ' cursors'
else
echo 'No matches for pattern: ' . a:pattern
endif
endfunction
command! -nargs=1 CursorsRegex call AddCursorsFromRegex(<q-args>)function! ColumnSelection(start_line, end_line, col) abort
let ranges = []
for lnum in range(a:start_line, a:end_line)
let line_len = len(getline(lnum))
if a:col <= line_len
call add(ranges, {
\ 'start': {'line': lnum - 1, 'character': a:col - 1},
\ 'end': {'line': lnum - 1, 'character': a:col - 1}
\ })
endif
endfor
call CocAction('addRanges', ranges)
echo 'Added ' . len(ranges) . ' cursors'
endfunction
command! -range -nargs=1 ColumnCursors call ColumnSelection(<line1>, <line2>, <args>)
" Usage: :10,20ColumnCursors 5function! SmartWordSelection() abort
let word = expand('<cword>')
let case_sensitive = word =~# '\u' " Check if word has uppercase
if case_sensitive
" Case-sensitive matching
let pattern = '\<' . word . '\>'
else
" Case-insensitive matching
let pattern = '\c\<' . word . '\>'
endif
" Find and select all matches
" Implementation similar to SelectAllOccurrences()
endfunction
nmap <silent> <leader>sw :call SmartWordSelection()<CR>Issue: Attempting to select word with no matches.
Solution:
function! SafeSelectOccurrences() abort
let word = expand('<cword>')
if empty(word)
echohl WarningMsg
echo 'No word under cursor'
echohl None
return
endif
" Count matches first
let count = 0
execute 'silent %s/' . word . '//gn'
" Check v:statusmsg for count
if count == 0
echo 'No occurrences of "' . word . '" found'
return
endif
" Proceed with selection
call SelectAllOccurrences()
endfunctionIssue: Adding cursors to invalid line/character positions.
Solution:
function! ValidateRanges(ranges) abort
let valid_ranges = []
let max_line = line('$') - 1
for range in a:ranges
let line = range.start.line
let char = range.start.character
" Skip invalid lines
if line < 0 || line > max_line
continue
endif
" Clamp character to line length
let line_text = getline(line + 1)
let max_char = strchars(line_text)
if char > max_char
let char = max_char
endif
call add(valid_ranges, {
\ 'start': {'line': line, 'character': char},
\ 'end': range.end
\ })
endfor
return valid_ranges
endfunctionIssue: Multi-cursor conflicts with insert mode or other modes.
Solution:
function! SafeActivateCursors() abort
" Ensure we're in normal mode
if mode() != 'n'
execute "normal! \<Esc>"
endif
" Check for conflicts
if exists('b:coc_snippet_active') && b:coc_snippet_active
echo 'Cannot activate multi-cursor while snippet is active'
return
endif
" Proceed with activation
call CocAction('cursorsSelect', bufnr('%'), 'n', 'word')
endfunctionSymptoms: Key mappings don't enter multi-cursor mode.
Solutions:
:echo exists('g:did_coc_loaded'):set modifiable?:verbose nmap <C-d>:CocInfoSymptoms: Multi-cursor mode exits unexpectedly during editing.
Solutions:
cursors.cancelKey setting in coc-settings.jsonlet g:coc_cursors_cancel_key = '<C-c>'Symptoms: Lag when editing with many cursors (>100).
Solutions:
{
"cursors.maxCount": 100
}:%s/pattern/replacement/g:syntax offSymptoms: Cursors don't align vertically in column selection.
Solutions:
<C-v> (select column)
<C-c> (convert to multi-cursor)let effective_col = virtcol('.')Symptoms: CursorsRegex finds no matches or wrong matches.
Solutions:
/patterncall AddCursorsFromRegex('\[.*\]') " Match [...]call AddCursorsFromRegex('\v(pattern)')Symptoms: Multi-cursor activates in different buffer.
Solutions:
call CocAction('cursorsSelect', bufnr('%'), 'n', 'word')if bufnr('%') != expected_bufnr
execute 'buffer' expected_bufnr
endif