autozimu / LanguageClient-neovim

Language Server Protocol (LSP) support for vim and neovim.
MIT License
3.55k stars 273 forks source link

Feature request: When jumping to a definition, increment the tag stack #517

Open dylan-chong opened 6 years ago

dylan-chong commented 6 years ago

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.

balta2ar commented 6 years ago

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

nnoremap <silent> gd :normal! m'<CR>:call LanguageClient_textDocument_definition()<CR>
dylan-chong commented 6 years ago

@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.

qrux0 commented 6 years ago

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 :-( .

amgoyal commented 5 years ago

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.

yen3 commented 5 years ago

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>
yen3 commented 5 years ago

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
nvlbg commented 2 years ago

I slightly modified the script by @yen3 to make it asynchronous:

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

  " Get the current position
  let pos = {
          \ 'fname': expand('%:p'),
          \ 'line': line("."),
          \ 'col': col("."),
          \ 'word': expand("<cword>")
          \ }

  call LanguageClient#textDocument_definition({'handle': v:true}, function('MyGoToDefinitionCallback', [pos]))
endfunction

function! MyGoToDefinitionCallback(pos, ...) abort
  " Get the original position
  let l:fname = a:pos['fname']
  let l:line = a:pos['line']
  let l:col = a:pos['col']
  let l:word = a:pos['word']

  " 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)

  " 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))
  call writefile([l:temp_tags_content], l:temp_tags_fname)

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

  " 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