Coc.nvim: Add support for vim's new `tagfunc` method

Created on 1 Aug 2019  路  25Comments  路  Source: neoclide/coc.nvim

Is your feature request related to a problem? Please describe.
When I used tags, I would often leverage the tagstack as opposed to the jumplist. An example would be using <C-]> to jump to a definition, then using a variety of [[/]] commands to jump around, and then when I am finished I can press <C-t> once to get back to where I was before the jump to definition call. This does not work if go-to-definition doesn't push onto the tagstack (currently, the jumplist is used instead).

Describe the solution you'd like
Rather than implement custom tagstack pushing/popping functionality, we should leverage vim's new tagfunc method (vim/vim#4010). I feel like this hooks into vim in a very native way, similar to #181 (which I 鉂わ笍, such a great change!!). By implementing a tagfunc, we will hook into all tag functionality and automatically pick up mappings like <C-w>].

Describe alternatives you've considered
I've considered using tools that manipulate the tagstack for you. But these tools/plugins are old and do not implement tagfunc. This means every tag mapping/command/etc needs to be overwritten to use the tool/plugin to manage the stack and then finally calling coc.nvim to make the jump. I'm also trying to dig down into coc.nvim code and see how I may go about implementing something like this on my own.

Additional context
Finding native ways to hook into vim is always a good thing. Similar to #181, I feel that this enhancement will improve the coc.nvim experience.

wontfix

Most helpful comment

Another hacky solution until a proper one is implemented:

https://github.com/astyagun/.vim/blob/0b1250ccd68c815811c683584a96a0977963b225/plugin_setup/coc.vim#L10-L31

function! s:gotoDefinition() abort
  let l:current_tag = expand('<cWORD>')

  let l:current_position    = getcurpos()
  let l:current_position[0] = bufnr()

  let l:current_tag_stack = gettagstack()
  let l:current_tag_index = l:current_tag_stack['curidx']
  let l:current_tag_items = l:current_tag_stack['items']

  if CocAction('jumpDefinition')
    let l:new_tag_index = l:current_tag_index + 1
    let l:new_tag_item = [#{tagname: l:current_tag, from: l:current_position}]
    let l:new_tag_items = l:current_tag_items[:]
    if l:current_tag_index <= len(l:current_tag_items)
      call remove(l:new_tag_items, l:current_tag_index - 1, -1)
    endif
    let l:new_tag_items += l:new_tag_item

    call settagstack(winnr(), #{curidx: l:new_tag_index, items: l:new_tag_items}, 'r')
  endif
endfunction

augroup Coc
  autocmd!
  autocmd FileType ruby nnoremap <buffer><silent> <C-]> :call <SID>gotoDefinition()<CR>
augroup end

All 25 comments

Thank you, but @chemzqm is too busy to do it recently, pull request is welcome.

We can add tag support for jumps, could be great if someone can make a PR.

function! coctagfunc#tagfunc(pattern, flags, info) abort
    if a:flags != "c"
        return v:null
    endif

    let name = expand("<cword>")
    execute("call CocAction('jumpDefinition')")
    let filename = expand('%:p')
    let cursor_pos = getpos(".")
    execute("normal \<C-o>")
    return [ { 'name': name, 'filename': filename, 'cmd': string(cursor_pos[1]) } ]
endfunction

This was the TERRIBLY crude POC I was thinking of an it does seem to work! Preferably, I'd find a way to get the filename and cursor_pos without making the jump and then jumping back.

Also, it would be nice to find out how to handle tag identifiers when no cursor position or context is available. But, I see this as a phase 2. I think the right thing to do here may be to essentially perform the symbols search that CocList provides.

Edit: Also, using line number for cmd is not ideal. I think you can use a search pattern here that will land you on the right column number as well.

using line number for cmd is not ideal.

It should support lnum with col, search pattern could be wrong as well.

@jbbudzon you can either use a search pattern like printf('/\%%%dl\%%%dc/', 3, 4) (i.e. /\%3l\%4c/) or a command like call cursor(3, 4)| (need the trailing bar). I'm not sure which is better.

function! coctagfunc#tagfunc(pattern, flags, info) abort
    if a:flags != "c"
        return v:null
    endif

    let name = expand("<cword>")
    execute("call CocAction('jumpDefinition')")
    let filename = expand('%:p')
    let cursor_pos = getpos(".")
    let cmd = '/\%'.cursor_pos[1].'l\%'.cursor_pos[2].'c/'
    execute("normal \<C-o>")
    return [ { 'name': name, 'filename': filename, 'cmd': cmd } ]
endfunction

I've actually been using this snippet above for some time with quite a bit of success. As I thought about it more and more, I think it may be difficult to handle cases where the c flag is not set (ie; when this command was not called from normal mode). In this case, no cursor context is available and the user could have triggered tagfunc in a variety of methods that may not be entirely compatible with coc.nvim's design/practices.

I maintain a tags file along side coc.nvim, so the above method works well for me.

@jbbudzon I assume you would just issue an lsp workspace/symbol and collect all the locations. What is the difficulty with that approach?

But to do :tag with no args you'd probably need to use the user_data.

I had thought of something similar involving workspace/symbol. Just never saw it to fruition. Admittedly, my current solution is a giant hack.

There are definitely some odd cases as you've pointed out, but I'm sure given enough love they could be overcome. The most bizarre scenario I could think of (which is potentially just more of an initialization issue) was launching vim with a tag parameter (ie; vim -t SymbolName).

I'd like to use this feature, is there any beta release or testing for this one? it would make my life so much easier as I love the tagstack of ctags.

Anyone?

@shuidixu Nothing official yet. This was not intended to be taken as a feature as it is poorly designed. Rather, I just wanted to spark some discussion and show a proof of concept that works with vim tagfunc as it was new at the time.

The proper way to do this is on the node.js side of things (and there is still more tagfunc functionality that is missing that needs implemented). As seen in #1380, the community is making some progress moving this functionality into coc.nvim proper. Perhaps, you'd be able to help out with coding/testing/documentation?

If you really want tagstack with coc.nvim as I did, I suggest dropping my VimL somewhere in your vimrc or in a homemade plugin or something.

Another hacky solution until a proper one is implemented:

https://github.com/astyagun/.vim/blob/0b1250ccd68c815811c683584a96a0977963b225/plugin_setup/coc.vim#L10-L31

function! s:gotoDefinition() abort
  let l:current_tag = expand('<cWORD>')

  let l:current_position    = getcurpos()
  let l:current_position[0] = bufnr()

  let l:current_tag_stack = gettagstack()
  let l:current_tag_index = l:current_tag_stack['curidx']
  let l:current_tag_items = l:current_tag_stack['items']

  if CocAction('jumpDefinition')
    let l:new_tag_index = l:current_tag_index + 1
    let l:new_tag_item = [#{tagname: l:current_tag, from: l:current_position}]
    let l:new_tag_items = l:current_tag_items[:]
    if l:current_tag_index <= len(l:current_tag_items)
      call remove(l:new_tag_items, l:current_tag_index - 1, -1)
    endif
    let l:new_tag_items += l:new_tag_item

    call settagstack(winnr(), #{curidx: l:new_tag_index, items: l:new_tag_items}, 'r')
  endif
endfunction

augroup Coc
  autocmd!
  autocmd FileType ruby nnoremap <buffer><silent> <C-]> :call <SID>gotoDefinition()<CR>
augroup end

@astyagun's solution works perfectly with vim but fails with nvim.
fails with:

Error detected while processing function <SNR>25_gotoDefinition:
line   12:
E121: Undefined variable: #
E15: Invalid expression: [#{tagname: l:current_tag, from: l:current_position}]
Press ENTER or type command to continue

Another hacky solution until a proper one is implemented:

  autocmd FileType ruby nnoremap <buffer><silent> <C-]> :call <SID>gotoDefinition()<CR>

The only downside to the solution presented above is that it does not actually leverage tagfunc, which was the primary focus of this issue. By using settagstack and not tagfunc, all tag-based mappings will need to be mapped manually.

For example, I often use <C-W>] to jump to the definition in a horizontal split which the original code handles and the code above does not.

Edit: Many edits, just providing clarifications and better markdown.

E121: Undefined variable: #
E15: Invalid expression: [#{tagname: l:current_tag, from: l:current_position}]

To the best of my understanding, this is because neovim doesn't yet support vim's literal-Dict.

E121: Undefined variable: #
E15: Invalid expression: [#{tagname: l:current_tag, from: l:current_position}]

To the best of my understanding, this is because neovim doesn't yet support vim's literal-Dict.

Yes, just wanted to point it out for anyone looking for a solution and finding it doesn't work on nvim

Thanks for adding the CocTagFunc @chemzqm
I think the sample config in the README should also be updated to include set tagfunc=CocTagFunc.

This is awesome! I'm not too familiar with tagfunc but is it possible to have this functionality extend to more of the coc goto functions (e.g. <Plug>(coc-type-definition), <Plug>(coc-references), etc.) short of wrapping them all like was shown in https://github.com/neoclide/coc.nvim/issues/1054#issuecomment-590820007?

@bmon No, it doesn't make sense.

I think you might have meant to tag @bmwill

Sorry to dig this up, but is there a way to implement this in neovim, since there's no tagfunc?

@gegnew Neovim already supports tagfunc: https://github.com/neovim/neovim/pull/11199

Despite having the most recent version of neovim, I did not have tagfunc....weird. Tracking HEAD now and I have it. Thanks!

Another hacky solution until a proper one is implemented:

https://github.com/astyagun/.vim/blob/0b1250ccd68c815811c683584a96a0977963b225/plugin_setup/coc.vim#L10-L31

function! s:gotoDefinition() abort
  let l:current_tag = expand('<cWORD>')

  let l:current_position    = getcurpos()
  let l:current_position[0] = bufnr()

  let l:current_tag_stack = gettagstack()
  let l:current_tag_index = l:current_tag_stack['curidx']
  let l:current_tag_items = l:current_tag_stack['items']

  if CocAction('jumpDefinition')
    let l:new_tag_index = l:current_tag_index + 1
    let l:new_tag_item = [#{tagname: l:current_tag, from: l:current_position}]
    let l:new_tag_items = l:current_tag_items[:]
    if l:current_tag_index <= len(l:current_tag_items)
      call remove(l:new_tag_items, l:current_tag_index - 1, -1)
    endif
    let l:new_tag_items += l:new_tag_item

    call settagstack(winnr(), #{curidx: l:new_tag_index, items: l:new_tag_items}, 'r')
  endif
endfunction

augroup Coc
  autocmd!
  autocmd FileType ruby nnoremap <buffer><silent> <C-]> :call <SID>gotoDefinition()<CR>
augroup end

this one is good, if you are in neovim now, (but you should change the dict syntax).
neovim's tagfunc is somehow not set the 'c' flags.
And this one has another benefit if you want keep gotoDefinition and gotoImplementation tagstack the same time.

for completeness, you can do something like this

function! s:goto_tag(tagkind) abort
  let tagname = expand('<cWORD>')
  let winnr = winnr()
  let pos = getcurpos()
  let pos[0] = bufnr()

  if CocAction('jump' . a:tagkind)
    call settagstack(winnr, { 
      \ 'curidx': gettagstack()['curidx'], 
      \ 'items': [{'tagname': tagname, 'from': pos}] 
      \ }, 't')
  endif
endfunction
nmap gd :call <SID>goto_tag("Definition")<CR>
nmap gi :call <SID>goto_tag("Implementation")<CR>
nmap gr :call <SID>goto_tag("References")<CR>
Was this page helpful?
0 / 5 - 0 ratings

Related issues

MaskRay picture MaskRay  路  3Comments

LinArcX picture LinArcX  路  4Comments

zhou13 picture zhou13  路  3Comments

czepluch picture czepluch  路  3Comments

hackingcat picture hackingcat  路  3Comments