4

What I expect from "tab-tag" navigation is the following behaviour: when need to jump to a tag:

  • if the corresponding file is already opened in a window, navigate to it
  • if it is not, open the buffer in a new tab

This is the behaviour of this command I like:

:tab drop file.txt

But it is not the behaviour of <C-]>.

How may I get this navigation style?


I am aware that there already are plenty of related questions on the topic, none of which provides the exact behaviour I am looking for.


This is now the object of a feature request to vim, unless I have been missing something :\

  • 1
    Does setting 'switchbuf'' help? – Christian Brabandt Oct 23 '16 at 12:07
  • @ChristianBrabandt It does not. Setting switchbuf=usetab or switchbuf=newtab has no effect on <c-]> behaviour :( – iago-lito Oct 23 '16 at 12:58
5

Put this in your vimrc:

map <C-]> :TabExpand 1<CR>

command -nargs=1 TabExpand call HandleTabTagExpand( <f-args> )

let s:commentchar = """

function HandleTabTagExpand(tagnumber)
    let tagident = expand("<cword>")
    redir @a
    try
        sil exe "tselect ".tagident
    catch /^Vim(\a\+):E433:/ " no tag file
        echom "No tag file found."
        return
    catch /^Vim(\a\+):E426:/ " tag not found
        echom "Tag not found."
        return
    endtry
    redir END
    let tagresults = split(@a, "\n")
    let tagmatches = []
    let linenum = 0
    for line in tagresults
        if linenum % 3 != 1
            " every third line contains the file names
            let linenum = linenum + 1
            continue
        endif
        " figure out where the filename actually starts
        " (it's usually column 32, but it might be farther)
        " one before 32 is 31, but index is 30 since arrays begin at zero
        let filestart = 30
        let c = line[filestart]
        while (filestart < strlen(line))
            let filestart = filestart + 1
            if c == " "
                break
            endif
            let c = line[filestart]
        endwhile
        " store the parsed match in an array
        call add(tagmatches, strpart(line, filestart))
        let linenum = linenum + 1
    endfor
    " navigate to the match specified by tagnumber
    try
        exe "tab drop ".tagmatches[a:tagnumber-1]
    catch /^Vim(\a\+):E471:/ " argument required (means no tag found)
        echom "Tag not found."
    endtry
    let done = 0
    let matchcount = 0
    let f_line = ""
    while done < 1 && matchcount < 1000
        sil exe "/".tagident
        let f_line = getline(".")
        let matchcount = matchcount + 1
        if match(f_line, "^\s+".s:commentchar) < 0
            let done = 1
        endif
    endwhile
    let f_index = stridx( f_line, tagident )
    sil exe "normal 0"
    sil exe "normal ".f_index."l"
endfunction

What this does is silently execute tselect, redirecting its output to register a. Tselect does the same thing as tag or tjump but it doesn't actually perform the jump, only the lookup, and returns a list of results. Without using :redir those results wouldn't be stored anywhere that we can get at them.

Next, it pulls from register a the tag output returned from tselect and parses it into a line by line array. The output contains other stuff we don't care about. All we need to know is that every second line (mod 3) has a matching file name starting (normally) at column 32, except where the other stuff in front is longer, so we have to account for that by finding the first space beginning at column 31. The character after the first space is where the file will always begin, regardless of what preceeds it.

Finally we parse the file names one by one, and pass one of those filenames to tab drop, which should open a new tab if one isn't already open, or jump over to the already-open tab if there is one.

Now you can just type

:TabExpand n

(where n is an integer) and it will open existing tab if there is one, or a new tab, for the nth matching tag. The -1 in the array address gives us more user-friendly behavior: when we give it a 1, meaning the first match, it results in zero which is the actual first index in the array.

If you want to jump immediately to the first tag match, you can use the default mapping for tag jump (<C-]>) because we have remapped it to call :TabExpand 1.

The last part of the code ignores commented lines (based on the specified comment character), and jumps to the exact line and cursor location of the next tag match. You can press n to cycle through further matches in the same file.

  • Thank you for this insight into the inner tag functionnalities. However, this function opens the.. tag file ./tags instead of opening the destination file, right? &tags contains ./tags,./TAGS,$HOME/.vimtags,tags,TAGS on my machine, so the last command is just tab drop ./tags for me, whereas I am expecting to open, say definitions.[hpp|py|java|R|whatever]. How is the result of tselect supposed to be used in your procedure? – iago-lito Nov 20 '16 at 10:56
  • Thanks for pointing that out. There has got to be somewhere that vim actually stores the tag lookup results. Let me take a look and I'll update my answer accordingly. I must have seen something on my machine that looked like a tag result instead of a tag file. EDIT: Answer has been updated and is verified to now work. – Tim Nov 20 '16 at 19:29
  • Thanks for this deep work into the topic :) Too bad that the function does not navigate to the actual tag location. – iago-lito Nov 22 '16 at 7:41
  • @Iago-lito I plan to update it with that feature, just haven't had time. – Tim Nov 22 '16 at 16:04
  • @lago-lito I've updated my answer so that it jumps to the exact tag location. – Tim Nov 25 '16 at 16:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.