Languageclient-neovim: Feature request: When jumping to a definition, increment the tag stack

Created on 15 Jul 2018  路  6Comments  路  Source: autozimu/LanguageClient-neovim

This feature would be really useful for jumping back to where you came from, when you do multiple jumps and browse around in various files.

Just a note, C-]which pops the tag stack, is different to C-o, which is not necessarily jump-to-definition-related. The stack for C-o gets modified very often, for example when doing gg , so simply using this key would not be ideal, as I often have to press the scheme the times to come back to where I was before using the jump to definition feature.

feature request help wanted

Most helpful comment

The jedi-vim uses a trick to insert tags into vim's original stack. The benefit of the method is that the implementation does not need to manipulate a stack to memory user's behaviour.

But the function is still vim version. In next step, I would lean rust in my free time and try to write rust version.

function! MyGoToDefinition(...) abort
  " ref: https://github.com/davidhalter/jedi-vim/blob/master/pythonx/jedi_vim.py#L329-L345

  " Get the current position
  let l:fname = expand('%:p')
  let l:line = line(".")
  let l:col = col(".")
  let l:word = expand("<cword>")

  " Call the original function to jump to the definition
  let l:result = LanguageClient_runSync(
                  \ 'LanguageClient#textDocument_definition', {
                  \ 'handle': v:true,
                  \ })

  " Get the position of definition
  let l:jump_fname = expand('%:p')
  let l:jump_line = line(".")
  let l:jump_col = col(".")

  " If the position is the same as previous, ignore the jump action
  if l:fname == l:jump_fname && l:line == l:jump_line
    return
  endif

  " Workaround: Jump to origial file. If the function is in rust, there is a
  " way to ignore the behaviour
  if &modified
    exec 'hide edit'  l:fname
  else
    exec 'edit' l:fname
  endif
  call cursor(l:line, l:col)

  " Store the original setting
  let l:ori_wildignore = &wildignore
  let l:ori_tags = &tags

  " Write a temp tags file
  let l:temp_tags_fname = tempname()
  let l:temp_tags_content = printf("%s\t%s\t%s", l:word, l:jump_fname,
      \ printf("call cursor(%d, %d)", l:jump_line, l:jump_col+1))
  call writefile([l:temp_tags_content], l:temp_tags_fname)

  " Set temporary new setting
  set wildignore=
  let &tags = l:temp_tags_fname

  " Add to new stack
  execute ":tjump " . l:word

  " Restore original setting
  let &tags = l:ori_tags
  let &wildignore = l:ori_wildignore

  " Remove temporary file
  if filereadable(l:temp_tags_fname)
    call delete(l:temp_tags_fname, "rf")
  endif
endfunction

nnoremap gd :call MyGoToDefinition()<cr>
" No need to remap <c-t> anymore

All 6 comments

Adding this was enough in my case, maybe it helps you too:

nnoremap <silent> gd :normal! m'<CR>:call LanguageClient_textDocument_definition()<CR>

@balta2ar Does that just create a mark? Marks are local to the current buffer, so if you go to the definition to a different file (so 90% of the time) then you won't be able to jump back.

Looks like vim/neovim doesn't have plugin API for tagstack interaction. (https://github.com/neovim/neovim/issues/398)

All ctrl+] and ctrl+t stuff is hardcoded, and there is no way how to implement this feature except hooking these hotkeys and emulate tagstack behavior :-( .

It seems that vim now have api's for incrementing tagstack, see the reference implementation here
https://github.com/natebosch/vim-lsc/blob/b8a04ab0721c7383290ee997dd3bd491b8f5f93c/autoload/lsc/reference.vim#L22-L28

it will be good to add now.

Thanks for @qrux0 and @amgoyal's comment. They give me the idea.

Maybe we don't have to rewrite <c-]> and <c-t>. We can add two function to push/pop the trace stack. I copy the concept and part of code from faith/vim-go and write the two proof-of-concept functions.

I have no idea about rust. If the idea is ok, I would try to learn rust and write rust version.

" ref: https://github.com/fatih/vim-go/blob/8b4b0e3cea0f41aeb6810fbc30d57e4a639d1ee6/autoload/go/def.vim

let s:lsp_stack = []
let s:lsp_stack_level = 0

function! MyGoToDefinition(...) abort
  " Get the current position
  let l:fname = expand('%:p')
  let l:line = line(".")
  let l:col = col(".")

  " Call the original function to jump to the definition
  let l:result = LanguageClient_runSync('LanguageClient#textDocument_definition', {
              \ 'handle': v:true,
              \ })

  " Get the position of definition
  let l:jump_fname = expand('%:p')
  let l:jump_line = line(".")
  let l:jump_col = col(".")

  " If the position is the same as previous, ignore the jump action
  if l:fname == l:jump_fname && l:line == l:jump_line
     return
  endif

  " Remove anything newer than the current position, just like basic
  " vim tag support
  if s:lsp_stack_level == 0
    let s:lsp_stack = []
  else
    let s:lsp_stack = s:lsp_stack[0:s:lsp_stack_level-1]
  endif

  " Push entry into stack
  let s:lsp_stack_level += 1
  let l:stack_entry = {'line': l:line, 'col': l:col, 'file': l:fname}
  call add(s:lsp_stack, l:stack_entry)
endfunction

function! MyTagStackPop() abort
  if s:lsp_stack_level == 0
    echo "lsp stack empty!"
    return
  endif

  " Get previous position
  let l:curr_stack_level = s:lsp_stack_level - 1
  let l:jump_entry = s:lsp_stack[l:curr_stack_level]

  " Pop stack
  let s:lsp_stack = s:lsp_stack[0:l:curr_stack_level]
  let s:lsp_stack_level = s:lsp_stack_level - 1

  " Jump to previous location
  if &modified
    exec 'hide edit'  l:jump_entry['file']
  else
    exec 'edit' l:jump_entry['file']
  endif

  call cursor(l:jump_entry['line'], l:jump_entry['col'])
  normal! zz
endfunction

nnoremap gd :call MyGoToDefinition()<cr>
nnoremap gt :<C-U>call MyTagStackPop()<cr>
"nnoremap <c-]> :call MyGoToDefinition()<cr>
"nnoremap <c-t> :<C-U>call MyTagStackPop()<cr>

The jedi-vim uses a trick to insert tags into vim's original stack. The benefit of the method is that the implementation does not need to manipulate a stack to memory user's behaviour.

But the function is still vim version. In next step, I would lean rust in my free time and try to write rust version.

function! MyGoToDefinition(...) abort
  " ref: https://github.com/davidhalter/jedi-vim/blob/master/pythonx/jedi_vim.py#L329-L345

  " Get the current position
  let l:fname = expand('%:p')
  let l:line = line(".")
  let l:col = col(".")
  let l:word = expand("<cword>")

  " Call the original function to jump to the definition
  let l:result = LanguageClient_runSync(
                  \ 'LanguageClient#textDocument_definition', {
                  \ 'handle': v:true,
                  \ })

  " Get the position of definition
  let l:jump_fname = expand('%:p')
  let l:jump_line = line(".")
  let l:jump_col = col(".")

  " If the position is the same as previous, ignore the jump action
  if l:fname == l:jump_fname && l:line == l:jump_line
    return
  endif

  " Workaround: Jump to origial file. If the function is in rust, there is a
  " way to ignore the behaviour
  if &modified
    exec 'hide edit'  l:fname
  else
    exec 'edit' l:fname
  endif
  call cursor(l:line, l:col)

  " Store the original setting
  let l:ori_wildignore = &wildignore
  let l:ori_tags = &tags

  " Write a temp tags file
  let l:temp_tags_fname = tempname()
  let l:temp_tags_content = printf("%s\t%s\t%s", l:word, l:jump_fname,
      \ printf("call cursor(%d, %d)", l:jump_line, l:jump_col+1))
  call writefile([l:temp_tags_content], l:temp_tags_fname)

  " Set temporary new setting
  set wildignore=
  let &tags = l:temp_tags_fname

  " Add to new stack
  execute ":tjump " . l:word

  " Restore original setting
  let &tags = l:ori_tags
  let &wildignore = l:ori_wildignore

  " Remove temporary file
  if filereadable(l:temp_tags_fname)
    call delete(l:temp_tags_fname, "rf")
  endif
endfunction

nnoremap gd :call MyGoToDefinition()<cr>
" No need to remap <c-t> anymore
Was this page helpful?
0 / 5 - 0 ratings