tessl install tessl/npm-coc-nvim@0.0.0LSP based intellisense engine for neovim & vim8.
Complexity: Advanced | Category: Advanced | Keywords: cursor, position, movement, selection, LSP
Common Tasks: Get cursor position | Move to position | Handle selections | Convert coordinates
Utilities for working with cursor position, movement, and selection in Vim/Neovim. These functions provide character-aware cursor operations that handle multibyte characters correctly.
let [line, char] = coc#cursor#position()Returns 0-indexed line and character position.
call coc#cursor#move_to(line, character)Move to specific line and character (0-indexed).
let selection = coc#cursor#get_selection(0)
let [start_line, start_char, end_line, end_char] = selectionReturns selection boundaries in LSP format.
let offset = coc#cursor#char_offset()Total character count from beginning of file.
Get cursor position relative to the screen.
/**
* Get cursor position relative to screen cell (absolute screen coordinates)
* @returns [row, col] array with 0-indexed screen coordinates
*/
function! coc#cursor#screen_pos()Usage Example:
let [row, col] = coc#cursor#screen_pos()
echo 'Cursor at screen position:' row ',' colMove cursor horizontally by a number of columns.
/**
* Move cursor by column offset (handles positive and negative deltas)
* @param delta - Number of columns to move (positive=right, negative=left)
*/
function! coc#cursor#move_by_col(delta)Usage Example:
" Move cursor 5 columns to the right
call coc#cursor#move_by_col(5)
" Move cursor 3 columns to the left
call coc#cursor#move_by_col(-3)Get current cursor position in LSP format (0-indexed line and character).
/**
* Get cursor position in LSP format
* @returns [line, character] array (0-indexed line, character count not bytes)
*/
function! coc#cursor#position()Usage Example:
let [line, char] = coc#cursor#position()
echo 'Cursor at line' line 'character' char
" Use with CocAction
call CocAction('doHover')Note: This function returns character positions, not byte positions, making it safe for use with multibyte characters (e.g., emoji, CJK characters).
Move cursor to a specific line and character position (LSP format).
/**
* Move cursor to LSP position (0-indexed)
* @param line - Line number (0-indexed)
* @param character - Character position (0-indexed, character count not bytes)
*/
function! coc#cursor#move_to(line, character)Usage Example:
" Move to line 10, character 5 (LSP format)
call coc#cursor#move_to(9, 5)
" Move to start of line 0
call coc#cursor#move_to(0, 0)
" Use with LSP response
let location = response.location
call coc#cursor#move_to(location.range.start.line, location.range.start.character)Get the character offset of the cursor from the start of the buffer.
/**
* Get character offset of cursor from buffer start
* Counts characters, not bytes (multibyte-safe)
* @returns Character offset (0-indexed from buffer start)
*/
function! coc#cursor#char_offset()Usage Example:
let offset = coc#cursor#char_offset()
echo 'Cursor is at character offset' offset 'from start of buffer'
" Useful for text document identification
let params = {
\ 'textDocument': {'uri': 'file://' . expand('%:p')},
\ 'position': {'character': offset}
\ }Note: This function counts actual characters (including newlines), not byte positions. Essential for LSP operations with multibyte text.
Get the latest visual selection range in LSP format.
/**
* Get latest selection range (from last visual mode)
* @param char - 1 to force character-wise mode, 0 to use actual visual mode
* @returns [start_line, start_char, end_line, end_char] or v:null if no selection
* All values are 0-indexed, character-based (not bytes)
*/
function! coc#cursor#get_selection(char)Usage Examples:
" Get last visual selection
let selection = coc#cursor#get_selection(0)
if selection isnot v:null
let [start_line, start_char, end_line, end_char] = selection
echo 'Selection from' start_line ':' start_char 'to' end_line ':' end_char
endif
" Force character-wise selection
let char_selection = coc#cursor#get_selection(1)
" Use with LSP range operations
function! ApplyRangeAction() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
echo 'No selection'
return
endif
let [sl, sc, el, ec] = selection
let range = {
\ 'start': {'line': sl, 'character': sc},
\ 'end': {'line': el, 'character': ec}
\ }
call CocAction('codeAction', '', range)
endfunctionVisual Modes:
v (character-wise): Returns character range within linesV (line-wise): Returns full line range with end character = 0\<C-v> (block-wise): Returns rectangular selection rangeNote: Returns v:null if no visual selection exists. All positions use character counts, not byte offsets.
Convert between Vim cursor positions and LSP positions:
" Vim (1-indexed line, 1-indexed byte column) to LSP (0-indexed line, 0-indexed character)
let lsp_pos = coc#cursor#position()
" LSP to Vim
call coc#cursor#move_to(lsp_line, lsp_character)These functions handle multibyte characters correctly:
" Works correctly with emoji, CJK, etc.
call coc#cursor#move_to(0, 5) " Moves to 5th character, not 5th byte
let offset = coc#cursor#char_offset() " Character count, not byte count" Visual mode mapping for code actions on selection
vnoremap <silent> <leader>a :call <SID>VisualCodeAction()<CR>
function! s:VisualCodeAction() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
return
endif
let [sl, sc, el, ec] = selection
call CocAction('codeAction', '', {
\ 'start': {'line': sl, 'character': sc},
\ 'end': {'line': el, 'character': ec}
\ })
endfunction" Create textDocument/hover params
function! s:hover_params() abort
let [line, char] = coc#cursor#position()
return {
\ 'textDocument': {'uri': 'file://' . expand('%:p')},
\ 'position': {'line': line, 'character': char}
\ }
endfunction
" Create range params from selection
function! s:range_params() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
return v:null
endif
let [sl, sc, el, ec] = selection
return {
\ 'textDocument': {'uri': 'file://' . expand('%:p')},
\ 'range': {
\ 'start': {'line': sl, 'character': sc},
\ 'end': {'line': el, 'character': ec}
\ }
\ }
endfunction" Jump to definition and save position
function! SmartDefinition() abort
" Save current position
let [line, char] = coc#cursor#position()
let pos = {'file': expand('%:p'), 'line': line, 'character': char}
" Jump to definition
call CocAction('jumpDefinition')
" Save to jump stack
call add(g:jump_stack, pos)
endfunction
" Jump back
function! JumpBack() abort
if empty(g:jump_stack)
echo 'Jump stack empty'
return
endif
let pos = remove(g:jump_stack, -1)
execute 'edit' pos.file
call coc#cursor#move_to(pos.line, pos.character)
endfunction
let g:jump_stack = []
nnoremap <silent> gd :call SmartDefinition()<CR>
nnoremap <silent> gb :call JumpBack()<CR>" Move cursor relative to current position
function! RelativeMove(lines, chars) abort
let [line, char] = coc#cursor#position()
let new_line = line + a:lines
let new_char = char + a:chars
" Clamp to buffer bounds
if new_line < 0
let new_line = 0
endif
if new_line >= line('$')
let new_line = line('$') - 1
endif
call coc#cursor#move_to(new_line, max([0, new_char]))
endfunction
" Move 5 lines down, 2 chars right
call RelativeMove(5, 2)" Validate and normalize selection
function! ValidateSelection() abort
let sel = coc#cursor#get_selection(0)
if sel is v:null
echohl ErrorMsg
echo 'No active selection'
echohl None
return v:null
endif
let [sl, sc, el, ec] = sel
" Ensure start is before end
if sl > el || (sl == el && sc > ec)
let [sl, sc, el, ec] = [el, ec, sl, sc]
endif
return [sl, sc, el, ec]
endfunction" Check if cursor is visible on screen
function! IsCursorVisible() abort
let [screen_row, screen_col] = coc#cursor#screen_pos()
let screen_lines = &lines - &cmdheight - 1
return screen_row >= 0 && screen_row < screen_lines
endfunction
" Center cursor on screen
function! CenterCursor() abort
normal! zz
let [row, col] = coc#cursor#screen_pos()
echo 'Cursor centered at screen position' row ',' col
endfunction" Get word under cursor in LSP coordinates
function! GetWordRange() abort
let [line, char] = coc#cursor#position()
let line_text = getline('.')
" Find word boundaries (simplified)
let start_char = char
while start_char > 0 && line_text[start_char - 1] =~# '\w'
let start_char -= 1
endwhile
let end_char = char
let line_len = strchars(line_text)
while end_char < line_len && line_text[end_char] =~# '\w'
let end_char += 1
endwhile
return {
\ 'start': {'line': line, 'character': start_char},
\ 'end': {'line': line, 'character': end_char}
\ }
endfunctionfunction! HighlightCurrentWord() abort
let [line, char] = coc#cursor#position()
let range = GetWordRange()
" Send highlight request to LSP
call CocAction('highlight', range)
endfunction
autocmd CursorHold * call HighlightCurrentWord()" Navigate to same column on different line
function! GotoLineKeepColumn(target_line) abort
let [_, char] = coc#cursor#position()
call coc#cursor#move_to(a:target_line, char)
endfunction
nnoremap <leader>j :call GotoLineKeepColumn(line('.') + 10)<CR>
nnoremap <leader>k :call GotoLineKeepColumn(line('.') - 10)<CR>function! RefactorSelection() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
echo 'Select text first'
return
endif
let [sl, sc, el, ec] = selection
let range = {
\ 'start': {'line': sl, 'character': sc},
\ 'end': {'line': el, 'character': ec}
\ }
call CocAction('refactor', range)
endfunction
vnoremap <silent> <leader>r :<C-u>call RefactorSelection()<CR>function! SelectCurrentLine() abort
let [line, _] = coc#cursor#position()
let line_text = getline('.')
let line_len = strchars(line_text)
return [line, 0, line, line_len]
endfunction
function! ExpandSelectionToLine() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
let selection = SelectCurrentLine()
else
let [sl, _, el, _] = selection
let start_len = strchars(getline(sl + 1))
let end_len = strchars(getline(el + 1))
let selection = [sl, 0, el, end_len]
endif
call CocAction('addRanges', [selection])
endfunctionIssue: coc#cursor#get_selection() returns v:null when no selection exists.
Solution:
function! SafeSelectionAction() abort
let selection = coc#cursor#get_selection(0)
if selection is v:null
echohl WarningMsg
echo 'No active selection. Make a visual selection first.'
echohl None
return
endif
" Process selection
let [sl, sc, el, ec] = selection
" ... rest of function
endfunctionIssue: Moving to invalid positions can cause errors.
Solution:
function! SafeMoveTo(line, char) abort
" Clamp line to valid range
let max_line = line('$') - 1
let safe_line = max([0, min([a:line, max_line])])
" Clamp character to line length
let line_text = getline(safe_line + 1)
let max_char = strchars(line_text)
let safe_char = max([0, min([a:char, max_char])])
call coc#cursor#move_to(safe_line, safe_char)
endfunctionIssue: Byte vs character confusion with multibyte text.
Solution:
" WRONG - uses byte offsets
let col = col('.') " Returns byte position
call cursor(line('.'), col + 5) " Wrong with multibyte chars
" RIGHT - uses character positions
let [line, char] = coc#cursor#position() " Character position
call coc#cursor#move_to(line, char + 5) " Correct with multibyteIssue: Screen coordinates differ from buffer coordinates.
Solution:
function! SafeScreenOperation() abort
let [screen_row, screen_col] = coc#cursor#screen_pos()
" Validate screen bounds
if screen_row < 0 || screen_row >= &lines
echo 'Cursor off screen vertically'
return
endif
if screen_col < 0 || screen_col >= &columns
echo 'Cursor off screen horizontally'
return
endif
" Safe to use screen coordinates
endfunctionSymptoms: Cursor jumps to wrong position or selections are off by characters.
Solutions:
coc#cursor#position() instead of line('.') and col('.')coc#cursor#move_to() instead of cursor() for LSP coordinatesstrchars() instead of strlen() for character counting:
" Wrong
let len = strlen(text) " Byte length
" Right
let len = strchars(text) " Character countSymptoms: coc#cursor#get_selection() returns null even after visual selection.
Solutions:
echo getpos("'<") " Start of last visual
echo getpos("'>") " End of last visualnormal! gvSymptoms: coc#cursor#screen_pos() returns unexpected values in window splits.
Solutions:
let win_row = winline() " Row in current window
let win_col = wincol() " Column in current windowSymptoms: Slow performance when moving cursor in large buffers.
Solutions:
noautocmd call coc#cursor#move_to(line, char)let save_eventignore = &eventignore
set eventignore=all
" ... perform movements
let &eventignore = save_eventignoreSymptoms: coc#cursor#char_offset() returns unexpected values at start/end of file.
Solutions:
if line('$') == 1 && getline(1) == ''
echo 'Empty buffer'
let offset = 0
else
let offset = coc#cursor#char_offset()
endiflet max_offset = strchars(join(getline(1, '$'), "\n"))
let offset = min([coc#cursor#char_offset(), max_offset])