A vim plugin to manage and view tag files

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
1
down vote

favorite












I'm writing a Vim plugin to manage and view tag files. There are two primary goals: gaining a bit more control over Easytags plugin by instructing it where to search and place tag files, and viewing a tree of tags that exist in a project.



The entire code can be seen here, in a frozen branch review-26-05-18.



I would like to have the basics reviewed before I go ahead and add bells and whistles. My main concerns are speed and robustness of my approach.



Here's the contents of plugin/guten-tag.vim, just some config used by the plugin:



if &cp || exists('g:loaded_guten_tag')
finish
endif
let g:loaded_guten_tag = 1

" Initialize the defaults - tag management
if !exists('g:guten_tag_forbidden_paths')
let g:guten_tag_forbidden_paths = ['v^/usr/.*', 'v^/etc/.*', 'v/var/.*']
endif
if !exists('g:guten_tag_markers')
let g:guten_tag_markers = ['.git', 'src', 'source']
endif
if !exists('g:guten_tag_tags_files')
let g:guten_tag_tags_files = ['tags', 'TAGS']
endif
if !exists('g:guten_tag_tags_when_forbidden')
let g:guten_tag_tags_when_forbidden = &tags
endif

" Initialize the defaults - tags exploration
if !exists('g:guten_tag_indent')
let g:guten_tag_indent = 2
endif
if !exists('g:guten_tag_split_style')
let g:guten_tag_split_style = 'vertical'
endif
if !exists('g:guten_tag_window_height')
let g:guten_tag_window_height = 30
endif
if !exists('g:gute_tag_window_width')
let g:guten_tag_window_width = 35
endif

augroup PluginGutenTag
autocmd!
autocmd BufRead,BufNewFile * call guten_tag#SetTagsPath()
augroup END

" Commands
command! GutenTagOpenTagWindow call guten_tag#CreateTagWindow()


Now for the actual working code. The first goal is accomplished by walking up the directory tree until one of 'marker files' is found, and then using a file in that directory as tags file. Here's the relevant parts of the code:



" Search up the directory tree until one of the file markers is found, and
" then uses this directory as a place to keep and look for tags file in.
function! guten_tag#SetTagsPath()
" First check if we're in an directory or a file which is forbidden from
" having local tags files.
let l:path = expand('%:p')
if s:ForbiddenLocations()
let &l:tags = g:guten_tag_tags_when_forbidden
return
endif
" Find a marker
while 1
let l:path = fnamemodify(l:path, ':h')
if s:HasMarkers(l:path)
break
endif
if l:path ==# '/'
return
endif
endwhile
" Make a tags option
let l:toplevel_tags =
for l:tagfile in g:guten_tag_tags_files
call add(l:toplevel_tags, l:path . '/' . l:tagfile)
endfor
let &l:tags = join(l:toplevel_tags, ',')
endfunction

" Test if a file matches any of the forbidden locations.
function! s:ForbiddenLocations()
let l:path = expand('%:p')
for l:pattern in g:guten_tag_forbidden_paths
if l:path =~# l:pattern
return 1
endif
endfor
return 0
endfunction

" Test if there is a marker in a given directory.
function! s:HasMarkers(dirpath)
for l:marker in g:guten_tag_markers
if !empty(globpath(a:dirpath, l:marker, 0, 1))
return 1
endif
endfor
return 0
endfunction


ForbiddenLocations thing is used to avoid placing tags files in certain places and falling back to global tags file in such cases.



The second goal is much harder, but it boils down to parsing a tags file and then building the hierarchy of tags inside. The tags are first separated by file they appear in, and then the said hierarchy is imposed on each segment based on some tag fields.



The main exported function for this is guten_tag#CreateTagWindow:



" Create a new window and populate it with tag tree
function! guten_tag#CreateTagWindow()
let l:files = tagfiles()
if len(l:files) == 0
return
endif
let l:file = l:files[0]
let l:tags = s:ParseFile(l:file)
let l:content = s:TagWindowContent(l:tags)
let l:window_name = s:MakeWindowName(l:file)
if bufexists(l:window_name)
return
endif
if g:guten_tag_split_style ==# 'vertical'
let l:command = g:guten_tag_window_width . 'vnew "' . l:window_name . '"'
else
let l:command = g:guten_tag_window_height . 'new "' . l:window_name . '"'
endif
exec l:command
call append(0, l:content)
setlocal buftype=nofile
setlocal nomodifiable
exec "file! " . l:window_name
endfunction


And here's the rest of the helpers, hopefully they are self-explanatory. If not, please say so, I'll add more detail then.



" Add a line describing a tag to a list of lines
function! s:AddLine(add_to, tag, indent_level, print_parent)
let l:kind = s:GetKind(a:tag)
let l:new_line = l:kind ==# '' ? '' : l:kind . ' '
let l:new_line = l:new_line . a:tag.name
if a:print_parent
let l:parent = s:GetParentName(a:tag)
if l:parent isnot# v:null
let l:new_line = l:new_line . ' (owned by ' . (l:parent) . ')'
endif
endif
let l:new_line = repeat(' ', a:indent_level) . l:new_line
call add(a:add_to, l:new_line)
call add(a:add_to, "")
endfunction

" Transform a flat list of tags into a dictionary where keys are file names
" and values are lists of tags appearing in this file.
function! s:BuildHierarchy(tags)
let l:res =
" First, extract top-level tags
for l:tag in a:tags
let l:parent = s:GetParentName(l:tag)
if l:parent is# v:null
call add(l:res, l:tag)
endif
endfor
" Now, attach non-top-level tags to them
for l:tags_in_file in values(s:SeparateTagsByFilename(a:tags))
for l:tag in l:tags_in_file
let l:parent_name = s:GetParentName(l:tag)
if l:parent_name is# v:null
continue
endif
let l:parent = s:TagByName(l:parent_name, l:tags_in_file)
if l:parent is# v:null
" If it's not possible to determine tag's parent, treat it as a top
" level tag.
call add(l:res, l:tag)
continue
endif
call add(l:parent.children, l:tag)
endfor
endfor
return s:SeparateTagsByFilename(l:res)
endfunction

" Get the name of a tag's parent, return v:null if there isn't one.
function! s:GetParentName(tag)
let l:fields = a:tag.fields
let l:fields_to_test = ['struct', 'class']
for l:fname in l:fields_to_test
let l:res = get(l:fields, l:fname, v:null)
if l:res isnot# v:null
return l:res
endif
endfor
return v:null
endfunction

" Get the first letter of 'kind' attribute
function! s:GetKind(tag)
if has_key(a:tag.fields, 'kind')
let l:kind = a:tag.fields.kind
else
return ""
endif
if l:kind ==# ""
return ""
else
return strcharpart(l:kind, 0, 1)
endif
endfunction

" Test if a tag should not appear in the hierarchy of tags
function! s:IgnoreTag(tag)
let l:ignore_kinds = ['i', 'import', 'include', 'namespace']
if !has_key(a:tag.fields, 'kind') || index(l:ignore_kinds, a:tag.fields.kind) ==# -1
return 0
else
return 1
endif
endfunction

" Generate a name for tag exploration window
function! s:MakeWindowName(tags_file)
return 'Guten Tag ' . fnamemodify(a:tags_file, ':p')
endfunction

" Parse a tag file into a sequence of top-level tags
function! s:ParseFile(file)
try
let l:lines = readfile(a:file)
catch /E484/
return
endtry
" Read all tags
let l:all_tags =
for l:line in l:lines
try
let l:new_tag = s:ParseLine(l:line)
catch /Comment line/
continue
endtry
if !s:IgnoreTag(l:new_tag)
call add(l:all_tags, l:new_tag)
endif
endfor
return s:BuildHierarchy(uniq(sort(l:all_tags)))
endfunction

" Parse a tag file line into a dict.
function! s:ParseLine(line)
if a:line =~# 'v^!.*'
throw 'Comment line'
endif
let l:extract_name_and_file = split(a:line, "t")
let l:res =
let l:res.name = l:extract_name_and_file[0]
let l:res.filename = l:extract_name_and_file[1]
let l:res.children =
let l:rest = join(l:extract_name_and_file[2:], "t")
" Extract EX search command
let l:extract_fields = split(l:rest, '"')
if len(l:extract_fields) <= 1
res.fields =
return res
endif
let l:res.search_cmd = l:extract_fields[0]
" Just in case the fields contain a '"', join them back
let l:extract_fields = join(l:extract_fields[1:], '"')
let l:fields =
for l:pair in split(l:extract_fields, "t")
let l:split = split(l:pair, ':')
if len(l:split) == 1
let l:key = 'kind'
let l:value = l:split[0]
else
let l:key = l:split[0]
let l:value = join(l:split[1:], ':')
endif
let l:fields[l:key] = l:value
endfor
let l:res.fields = l:fields
call s:TagPostprocessing(l:res)
return res
endfunction

" Return a dict where keys are filenames and values are lists of tags
" appearing in the corresponding file
function! s:SeparateTagsByFilename(tags)
let l:res =
for l:tag in a:tags
let l:file = l:tag.filename
if has_key(l:res, l:file)
call add(l:res[l:file], l:tag)
else
let l:res[l:file] = [l:tag]
endif
endfor
return l:res
endfunction

" Return the first tag with a given name from a list, or null if there's no
" such tag
function! s:TagByName(name, list)
for l:tag in a:list
if l:tag.name ==# a:name
return l:tag
endif
endfor
return v:null
endfunction

" Compare two tags by name
function! s:TagComparator(tag1, tag2)
let l:name1 = a:tag1.name
let l:name2 = a:tag2.name
if l:name1 ># l:name2
return 1
elseif l:name1 ==# l:name2
return 0
else
return -1
endif
endfunction

" Return a list of lines in the tag overview window
function! s:TagWindowContent(tags_hierarchy)
let l:res =
for l:file in keys(a:tags_hierarchy)
call add(l:res, fnamemodify(l:file, ':p'))
call add(l:res, "")
for l:toplevel in a:tags_hierarchy[l:file]
let l:indent = g:guten_tag_indent
call s:AddLine(l:res, l:toplevel, l:indent, 1)
let l:traversal = [[l:toplevel, 0]] " tag, current child index
let l:indent += g:guten_tag_indent
while len(l:traversal) ># 0
let [l:parent, l:child_index] = l:traversal[-1]
if l:child_index ==# len(l:parent.children)
unlet l:traversal[-1]
let l:indent -= g:guten_tag_indent
continue
endif
let l:cur = l:parent.children[l:child_index]
call s:AddLine(l:res, l:cur, l:indent, 0)
if len(l:cur.children) > 0
call add(l:traversal, [l:cur, 0])
let l:indent += g:guten_tag_indent
echomsg l:indent
continue
endif
let l:traversal[-1][1] += 1
endwhile
endfor
endfor
return l:res
endfunction

" Do some actions after all fields and attributes of a tag were read
function! s:TagPostprocessing(tag)
" Presently, do nothing. TODO: language-specific processing, like function
" signature extraction
endfunction






share|improve this question



























    up vote
    1
    down vote

    favorite












    I'm writing a Vim plugin to manage and view tag files. There are two primary goals: gaining a bit more control over Easytags plugin by instructing it where to search and place tag files, and viewing a tree of tags that exist in a project.



    The entire code can be seen here, in a frozen branch review-26-05-18.



    I would like to have the basics reviewed before I go ahead and add bells and whistles. My main concerns are speed and robustness of my approach.



    Here's the contents of plugin/guten-tag.vim, just some config used by the plugin:



    if &cp || exists('g:loaded_guten_tag')
    finish
    endif
    let g:loaded_guten_tag = 1

    " Initialize the defaults - tag management
    if !exists('g:guten_tag_forbidden_paths')
    let g:guten_tag_forbidden_paths = ['v^/usr/.*', 'v^/etc/.*', 'v/var/.*']
    endif
    if !exists('g:guten_tag_markers')
    let g:guten_tag_markers = ['.git', 'src', 'source']
    endif
    if !exists('g:guten_tag_tags_files')
    let g:guten_tag_tags_files = ['tags', 'TAGS']
    endif
    if !exists('g:guten_tag_tags_when_forbidden')
    let g:guten_tag_tags_when_forbidden = &tags
    endif

    " Initialize the defaults - tags exploration
    if !exists('g:guten_tag_indent')
    let g:guten_tag_indent = 2
    endif
    if !exists('g:guten_tag_split_style')
    let g:guten_tag_split_style = 'vertical'
    endif
    if !exists('g:guten_tag_window_height')
    let g:guten_tag_window_height = 30
    endif
    if !exists('g:gute_tag_window_width')
    let g:guten_tag_window_width = 35
    endif

    augroup PluginGutenTag
    autocmd!
    autocmd BufRead,BufNewFile * call guten_tag#SetTagsPath()
    augroup END

    " Commands
    command! GutenTagOpenTagWindow call guten_tag#CreateTagWindow()


    Now for the actual working code. The first goal is accomplished by walking up the directory tree until one of 'marker files' is found, and then using a file in that directory as tags file. Here's the relevant parts of the code:



    " Search up the directory tree until one of the file markers is found, and
    " then uses this directory as a place to keep and look for tags file in.
    function! guten_tag#SetTagsPath()
    " First check if we're in an directory or a file which is forbidden from
    " having local tags files.
    let l:path = expand('%:p')
    if s:ForbiddenLocations()
    let &l:tags = g:guten_tag_tags_when_forbidden
    return
    endif
    " Find a marker
    while 1
    let l:path = fnamemodify(l:path, ':h')
    if s:HasMarkers(l:path)
    break
    endif
    if l:path ==# '/'
    return
    endif
    endwhile
    " Make a tags option
    let l:toplevel_tags =
    for l:tagfile in g:guten_tag_tags_files
    call add(l:toplevel_tags, l:path . '/' . l:tagfile)
    endfor
    let &l:tags = join(l:toplevel_tags, ',')
    endfunction

    " Test if a file matches any of the forbidden locations.
    function! s:ForbiddenLocations()
    let l:path = expand('%:p')
    for l:pattern in g:guten_tag_forbidden_paths
    if l:path =~# l:pattern
    return 1
    endif
    endfor
    return 0
    endfunction

    " Test if there is a marker in a given directory.
    function! s:HasMarkers(dirpath)
    for l:marker in g:guten_tag_markers
    if !empty(globpath(a:dirpath, l:marker, 0, 1))
    return 1
    endif
    endfor
    return 0
    endfunction


    ForbiddenLocations thing is used to avoid placing tags files in certain places and falling back to global tags file in such cases.



    The second goal is much harder, but it boils down to parsing a tags file and then building the hierarchy of tags inside. The tags are first separated by file they appear in, and then the said hierarchy is imposed on each segment based on some tag fields.



    The main exported function for this is guten_tag#CreateTagWindow:



    " Create a new window and populate it with tag tree
    function! guten_tag#CreateTagWindow()
    let l:files = tagfiles()
    if len(l:files) == 0
    return
    endif
    let l:file = l:files[0]
    let l:tags = s:ParseFile(l:file)
    let l:content = s:TagWindowContent(l:tags)
    let l:window_name = s:MakeWindowName(l:file)
    if bufexists(l:window_name)
    return
    endif
    if g:guten_tag_split_style ==# 'vertical'
    let l:command = g:guten_tag_window_width . 'vnew "' . l:window_name . '"'
    else
    let l:command = g:guten_tag_window_height . 'new "' . l:window_name . '"'
    endif
    exec l:command
    call append(0, l:content)
    setlocal buftype=nofile
    setlocal nomodifiable
    exec "file! " . l:window_name
    endfunction


    And here's the rest of the helpers, hopefully they are self-explanatory. If not, please say so, I'll add more detail then.



    " Add a line describing a tag to a list of lines
    function! s:AddLine(add_to, tag, indent_level, print_parent)
    let l:kind = s:GetKind(a:tag)
    let l:new_line = l:kind ==# '' ? '' : l:kind . ' '
    let l:new_line = l:new_line . a:tag.name
    if a:print_parent
    let l:parent = s:GetParentName(a:tag)
    if l:parent isnot# v:null
    let l:new_line = l:new_line . ' (owned by ' . (l:parent) . ')'
    endif
    endif
    let l:new_line = repeat(' ', a:indent_level) . l:new_line
    call add(a:add_to, l:new_line)
    call add(a:add_to, "")
    endfunction

    " Transform a flat list of tags into a dictionary where keys are file names
    " and values are lists of tags appearing in this file.
    function! s:BuildHierarchy(tags)
    let l:res =
    " First, extract top-level tags
    for l:tag in a:tags
    let l:parent = s:GetParentName(l:tag)
    if l:parent is# v:null
    call add(l:res, l:tag)
    endif
    endfor
    " Now, attach non-top-level tags to them
    for l:tags_in_file in values(s:SeparateTagsByFilename(a:tags))
    for l:tag in l:tags_in_file
    let l:parent_name = s:GetParentName(l:tag)
    if l:parent_name is# v:null
    continue
    endif
    let l:parent = s:TagByName(l:parent_name, l:tags_in_file)
    if l:parent is# v:null
    " If it's not possible to determine tag's parent, treat it as a top
    " level tag.
    call add(l:res, l:tag)
    continue
    endif
    call add(l:parent.children, l:tag)
    endfor
    endfor
    return s:SeparateTagsByFilename(l:res)
    endfunction

    " Get the name of a tag's parent, return v:null if there isn't one.
    function! s:GetParentName(tag)
    let l:fields = a:tag.fields
    let l:fields_to_test = ['struct', 'class']
    for l:fname in l:fields_to_test
    let l:res = get(l:fields, l:fname, v:null)
    if l:res isnot# v:null
    return l:res
    endif
    endfor
    return v:null
    endfunction

    " Get the first letter of 'kind' attribute
    function! s:GetKind(tag)
    if has_key(a:tag.fields, 'kind')
    let l:kind = a:tag.fields.kind
    else
    return ""
    endif
    if l:kind ==# ""
    return ""
    else
    return strcharpart(l:kind, 0, 1)
    endif
    endfunction

    " Test if a tag should not appear in the hierarchy of tags
    function! s:IgnoreTag(tag)
    let l:ignore_kinds = ['i', 'import', 'include', 'namespace']
    if !has_key(a:tag.fields, 'kind') || index(l:ignore_kinds, a:tag.fields.kind) ==# -1
    return 0
    else
    return 1
    endif
    endfunction

    " Generate a name for tag exploration window
    function! s:MakeWindowName(tags_file)
    return 'Guten Tag ' . fnamemodify(a:tags_file, ':p')
    endfunction

    " Parse a tag file into a sequence of top-level tags
    function! s:ParseFile(file)
    try
    let l:lines = readfile(a:file)
    catch /E484/
    return
    endtry
    " Read all tags
    let l:all_tags =
    for l:line in l:lines
    try
    let l:new_tag = s:ParseLine(l:line)
    catch /Comment line/
    continue
    endtry
    if !s:IgnoreTag(l:new_tag)
    call add(l:all_tags, l:new_tag)
    endif
    endfor
    return s:BuildHierarchy(uniq(sort(l:all_tags)))
    endfunction

    " Parse a tag file line into a dict.
    function! s:ParseLine(line)
    if a:line =~# 'v^!.*'
    throw 'Comment line'
    endif
    let l:extract_name_and_file = split(a:line, "t")
    let l:res =
    let l:res.name = l:extract_name_and_file[0]
    let l:res.filename = l:extract_name_and_file[1]
    let l:res.children =
    let l:rest = join(l:extract_name_and_file[2:], "t")
    " Extract EX search command
    let l:extract_fields = split(l:rest, '"')
    if len(l:extract_fields) <= 1
    res.fields =
    return res
    endif
    let l:res.search_cmd = l:extract_fields[0]
    " Just in case the fields contain a '"', join them back
    let l:extract_fields = join(l:extract_fields[1:], '"')
    let l:fields =
    for l:pair in split(l:extract_fields, "t")
    let l:split = split(l:pair, ':')
    if len(l:split) == 1
    let l:key = 'kind'
    let l:value = l:split[0]
    else
    let l:key = l:split[0]
    let l:value = join(l:split[1:], ':')
    endif
    let l:fields[l:key] = l:value
    endfor
    let l:res.fields = l:fields
    call s:TagPostprocessing(l:res)
    return res
    endfunction

    " Return a dict where keys are filenames and values are lists of tags
    " appearing in the corresponding file
    function! s:SeparateTagsByFilename(tags)
    let l:res =
    for l:tag in a:tags
    let l:file = l:tag.filename
    if has_key(l:res, l:file)
    call add(l:res[l:file], l:tag)
    else
    let l:res[l:file] = [l:tag]
    endif
    endfor
    return l:res
    endfunction

    " Return the first tag with a given name from a list, or null if there's no
    " such tag
    function! s:TagByName(name, list)
    for l:tag in a:list
    if l:tag.name ==# a:name
    return l:tag
    endif
    endfor
    return v:null
    endfunction

    " Compare two tags by name
    function! s:TagComparator(tag1, tag2)
    let l:name1 = a:tag1.name
    let l:name2 = a:tag2.name
    if l:name1 ># l:name2
    return 1
    elseif l:name1 ==# l:name2
    return 0
    else
    return -1
    endif
    endfunction

    " Return a list of lines in the tag overview window
    function! s:TagWindowContent(tags_hierarchy)
    let l:res =
    for l:file in keys(a:tags_hierarchy)
    call add(l:res, fnamemodify(l:file, ':p'))
    call add(l:res, "")
    for l:toplevel in a:tags_hierarchy[l:file]
    let l:indent = g:guten_tag_indent
    call s:AddLine(l:res, l:toplevel, l:indent, 1)
    let l:traversal = [[l:toplevel, 0]] " tag, current child index
    let l:indent += g:guten_tag_indent
    while len(l:traversal) ># 0
    let [l:parent, l:child_index] = l:traversal[-1]
    if l:child_index ==# len(l:parent.children)
    unlet l:traversal[-1]
    let l:indent -= g:guten_tag_indent
    continue
    endif
    let l:cur = l:parent.children[l:child_index]
    call s:AddLine(l:res, l:cur, l:indent, 0)
    if len(l:cur.children) > 0
    call add(l:traversal, [l:cur, 0])
    let l:indent += g:guten_tag_indent
    echomsg l:indent
    continue
    endif
    let l:traversal[-1][1] += 1
    endwhile
    endfor
    endfor
    return l:res
    endfunction

    " Do some actions after all fields and attributes of a tag were read
    function! s:TagPostprocessing(tag)
    " Presently, do nothing. TODO: language-specific processing, like function
    " signature extraction
    endfunction






    share|improve this question























      up vote
      1
      down vote

      favorite









      up vote
      1
      down vote

      favorite











      I'm writing a Vim plugin to manage and view tag files. There are two primary goals: gaining a bit more control over Easytags plugin by instructing it where to search and place tag files, and viewing a tree of tags that exist in a project.



      The entire code can be seen here, in a frozen branch review-26-05-18.



      I would like to have the basics reviewed before I go ahead and add bells and whistles. My main concerns are speed and robustness of my approach.



      Here's the contents of plugin/guten-tag.vim, just some config used by the plugin:



      if &cp || exists('g:loaded_guten_tag')
      finish
      endif
      let g:loaded_guten_tag = 1

      " Initialize the defaults - tag management
      if !exists('g:guten_tag_forbidden_paths')
      let g:guten_tag_forbidden_paths = ['v^/usr/.*', 'v^/etc/.*', 'v/var/.*']
      endif
      if !exists('g:guten_tag_markers')
      let g:guten_tag_markers = ['.git', 'src', 'source']
      endif
      if !exists('g:guten_tag_tags_files')
      let g:guten_tag_tags_files = ['tags', 'TAGS']
      endif
      if !exists('g:guten_tag_tags_when_forbidden')
      let g:guten_tag_tags_when_forbidden = &tags
      endif

      " Initialize the defaults - tags exploration
      if !exists('g:guten_tag_indent')
      let g:guten_tag_indent = 2
      endif
      if !exists('g:guten_tag_split_style')
      let g:guten_tag_split_style = 'vertical'
      endif
      if !exists('g:guten_tag_window_height')
      let g:guten_tag_window_height = 30
      endif
      if !exists('g:gute_tag_window_width')
      let g:guten_tag_window_width = 35
      endif

      augroup PluginGutenTag
      autocmd!
      autocmd BufRead,BufNewFile * call guten_tag#SetTagsPath()
      augroup END

      " Commands
      command! GutenTagOpenTagWindow call guten_tag#CreateTagWindow()


      Now for the actual working code. The first goal is accomplished by walking up the directory tree until one of 'marker files' is found, and then using a file in that directory as tags file. Here's the relevant parts of the code:



      " Search up the directory tree until one of the file markers is found, and
      " then uses this directory as a place to keep and look for tags file in.
      function! guten_tag#SetTagsPath()
      " First check if we're in an directory or a file which is forbidden from
      " having local tags files.
      let l:path = expand('%:p')
      if s:ForbiddenLocations()
      let &l:tags = g:guten_tag_tags_when_forbidden
      return
      endif
      " Find a marker
      while 1
      let l:path = fnamemodify(l:path, ':h')
      if s:HasMarkers(l:path)
      break
      endif
      if l:path ==# '/'
      return
      endif
      endwhile
      " Make a tags option
      let l:toplevel_tags =
      for l:tagfile in g:guten_tag_tags_files
      call add(l:toplevel_tags, l:path . '/' . l:tagfile)
      endfor
      let &l:tags = join(l:toplevel_tags, ',')
      endfunction

      " Test if a file matches any of the forbidden locations.
      function! s:ForbiddenLocations()
      let l:path = expand('%:p')
      for l:pattern in g:guten_tag_forbidden_paths
      if l:path =~# l:pattern
      return 1
      endif
      endfor
      return 0
      endfunction

      " Test if there is a marker in a given directory.
      function! s:HasMarkers(dirpath)
      for l:marker in g:guten_tag_markers
      if !empty(globpath(a:dirpath, l:marker, 0, 1))
      return 1
      endif
      endfor
      return 0
      endfunction


      ForbiddenLocations thing is used to avoid placing tags files in certain places and falling back to global tags file in such cases.



      The second goal is much harder, but it boils down to parsing a tags file and then building the hierarchy of tags inside. The tags are first separated by file they appear in, and then the said hierarchy is imposed on each segment based on some tag fields.



      The main exported function for this is guten_tag#CreateTagWindow:



      " Create a new window and populate it with tag tree
      function! guten_tag#CreateTagWindow()
      let l:files = tagfiles()
      if len(l:files) == 0
      return
      endif
      let l:file = l:files[0]
      let l:tags = s:ParseFile(l:file)
      let l:content = s:TagWindowContent(l:tags)
      let l:window_name = s:MakeWindowName(l:file)
      if bufexists(l:window_name)
      return
      endif
      if g:guten_tag_split_style ==# 'vertical'
      let l:command = g:guten_tag_window_width . 'vnew "' . l:window_name . '"'
      else
      let l:command = g:guten_tag_window_height . 'new "' . l:window_name . '"'
      endif
      exec l:command
      call append(0, l:content)
      setlocal buftype=nofile
      setlocal nomodifiable
      exec "file! " . l:window_name
      endfunction


      And here's the rest of the helpers, hopefully they are self-explanatory. If not, please say so, I'll add more detail then.



      " Add a line describing a tag to a list of lines
      function! s:AddLine(add_to, tag, indent_level, print_parent)
      let l:kind = s:GetKind(a:tag)
      let l:new_line = l:kind ==# '' ? '' : l:kind . ' '
      let l:new_line = l:new_line . a:tag.name
      if a:print_parent
      let l:parent = s:GetParentName(a:tag)
      if l:parent isnot# v:null
      let l:new_line = l:new_line . ' (owned by ' . (l:parent) . ')'
      endif
      endif
      let l:new_line = repeat(' ', a:indent_level) . l:new_line
      call add(a:add_to, l:new_line)
      call add(a:add_to, "")
      endfunction

      " Transform a flat list of tags into a dictionary where keys are file names
      " and values are lists of tags appearing in this file.
      function! s:BuildHierarchy(tags)
      let l:res =
      " First, extract top-level tags
      for l:tag in a:tags
      let l:parent = s:GetParentName(l:tag)
      if l:parent is# v:null
      call add(l:res, l:tag)
      endif
      endfor
      " Now, attach non-top-level tags to them
      for l:tags_in_file in values(s:SeparateTagsByFilename(a:tags))
      for l:tag in l:tags_in_file
      let l:parent_name = s:GetParentName(l:tag)
      if l:parent_name is# v:null
      continue
      endif
      let l:parent = s:TagByName(l:parent_name, l:tags_in_file)
      if l:parent is# v:null
      " If it's not possible to determine tag's parent, treat it as a top
      " level tag.
      call add(l:res, l:tag)
      continue
      endif
      call add(l:parent.children, l:tag)
      endfor
      endfor
      return s:SeparateTagsByFilename(l:res)
      endfunction

      " Get the name of a tag's parent, return v:null if there isn't one.
      function! s:GetParentName(tag)
      let l:fields = a:tag.fields
      let l:fields_to_test = ['struct', 'class']
      for l:fname in l:fields_to_test
      let l:res = get(l:fields, l:fname, v:null)
      if l:res isnot# v:null
      return l:res
      endif
      endfor
      return v:null
      endfunction

      " Get the first letter of 'kind' attribute
      function! s:GetKind(tag)
      if has_key(a:tag.fields, 'kind')
      let l:kind = a:tag.fields.kind
      else
      return ""
      endif
      if l:kind ==# ""
      return ""
      else
      return strcharpart(l:kind, 0, 1)
      endif
      endfunction

      " Test if a tag should not appear in the hierarchy of tags
      function! s:IgnoreTag(tag)
      let l:ignore_kinds = ['i', 'import', 'include', 'namespace']
      if !has_key(a:tag.fields, 'kind') || index(l:ignore_kinds, a:tag.fields.kind) ==# -1
      return 0
      else
      return 1
      endif
      endfunction

      " Generate a name for tag exploration window
      function! s:MakeWindowName(tags_file)
      return 'Guten Tag ' . fnamemodify(a:tags_file, ':p')
      endfunction

      " Parse a tag file into a sequence of top-level tags
      function! s:ParseFile(file)
      try
      let l:lines = readfile(a:file)
      catch /E484/
      return
      endtry
      " Read all tags
      let l:all_tags =
      for l:line in l:lines
      try
      let l:new_tag = s:ParseLine(l:line)
      catch /Comment line/
      continue
      endtry
      if !s:IgnoreTag(l:new_tag)
      call add(l:all_tags, l:new_tag)
      endif
      endfor
      return s:BuildHierarchy(uniq(sort(l:all_tags)))
      endfunction

      " Parse a tag file line into a dict.
      function! s:ParseLine(line)
      if a:line =~# 'v^!.*'
      throw 'Comment line'
      endif
      let l:extract_name_and_file = split(a:line, "t")
      let l:res =
      let l:res.name = l:extract_name_and_file[0]
      let l:res.filename = l:extract_name_and_file[1]
      let l:res.children =
      let l:rest = join(l:extract_name_and_file[2:], "t")
      " Extract EX search command
      let l:extract_fields = split(l:rest, '"')
      if len(l:extract_fields) <= 1
      res.fields =
      return res
      endif
      let l:res.search_cmd = l:extract_fields[0]
      " Just in case the fields contain a '"', join them back
      let l:extract_fields = join(l:extract_fields[1:], '"')
      let l:fields =
      for l:pair in split(l:extract_fields, "t")
      let l:split = split(l:pair, ':')
      if len(l:split) == 1
      let l:key = 'kind'
      let l:value = l:split[0]
      else
      let l:key = l:split[0]
      let l:value = join(l:split[1:], ':')
      endif
      let l:fields[l:key] = l:value
      endfor
      let l:res.fields = l:fields
      call s:TagPostprocessing(l:res)
      return res
      endfunction

      " Return a dict where keys are filenames and values are lists of tags
      " appearing in the corresponding file
      function! s:SeparateTagsByFilename(tags)
      let l:res =
      for l:tag in a:tags
      let l:file = l:tag.filename
      if has_key(l:res, l:file)
      call add(l:res[l:file], l:tag)
      else
      let l:res[l:file] = [l:tag]
      endif
      endfor
      return l:res
      endfunction

      " Return the first tag with a given name from a list, or null if there's no
      " such tag
      function! s:TagByName(name, list)
      for l:tag in a:list
      if l:tag.name ==# a:name
      return l:tag
      endif
      endfor
      return v:null
      endfunction

      " Compare two tags by name
      function! s:TagComparator(tag1, tag2)
      let l:name1 = a:tag1.name
      let l:name2 = a:tag2.name
      if l:name1 ># l:name2
      return 1
      elseif l:name1 ==# l:name2
      return 0
      else
      return -1
      endif
      endfunction

      " Return a list of lines in the tag overview window
      function! s:TagWindowContent(tags_hierarchy)
      let l:res =
      for l:file in keys(a:tags_hierarchy)
      call add(l:res, fnamemodify(l:file, ':p'))
      call add(l:res, "")
      for l:toplevel in a:tags_hierarchy[l:file]
      let l:indent = g:guten_tag_indent
      call s:AddLine(l:res, l:toplevel, l:indent, 1)
      let l:traversal = [[l:toplevel, 0]] " tag, current child index
      let l:indent += g:guten_tag_indent
      while len(l:traversal) ># 0
      let [l:parent, l:child_index] = l:traversal[-1]
      if l:child_index ==# len(l:parent.children)
      unlet l:traversal[-1]
      let l:indent -= g:guten_tag_indent
      continue
      endif
      let l:cur = l:parent.children[l:child_index]
      call s:AddLine(l:res, l:cur, l:indent, 0)
      if len(l:cur.children) > 0
      call add(l:traversal, [l:cur, 0])
      let l:indent += g:guten_tag_indent
      echomsg l:indent
      continue
      endif
      let l:traversal[-1][1] += 1
      endwhile
      endfor
      endfor
      return l:res
      endfunction

      " Do some actions after all fields and attributes of a tag were read
      function! s:TagPostprocessing(tag)
      " Presently, do nothing. TODO: language-specific processing, like function
      " signature extraction
      endfunction






      share|improve this question













      I'm writing a Vim plugin to manage and view tag files. There are two primary goals: gaining a bit more control over Easytags plugin by instructing it where to search and place tag files, and viewing a tree of tags that exist in a project.



      The entire code can be seen here, in a frozen branch review-26-05-18.



      I would like to have the basics reviewed before I go ahead and add bells and whistles. My main concerns are speed and robustness of my approach.



      Here's the contents of plugin/guten-tag.vim, just some config used by the plugin:



      if &cp || exists('g:loaded_guten_tag')
      finish
      endif
      let g:loaded_guten_tag = 1

      " Initialize the defaults - tag management
      if !exists('g:guten_tag_forbidden_paths')
      let g:guten_tag_forbidden_paths = ['v^/usr/.*', 'v^/etc/.*', 'v/var/.*']
      endif
      if !exists('g:guten_tag_markers')
      let g:guten_tag_markers = ['.git', 'src', 'source']
      endif
      if !exists('g:guten_tag_tags_files')
      let g:guten_tag_tags_files = ['tags', 'TAGS']
      endif
      if !exists('g:guten_tag_tags_when_forbidden')
      let g:guten_tag_tags_when_forbidden = &tags
      endif

      " Initialize the defaults - tags exploration
      if !exists('g:guten_tag_indent')
      let g:guten_tag_indent = 2
      endif
      if !exists('g:guten_tag_split_style')
      let g:guten_tag_split_style = 'vertical'
      endif
      if !exists('g:guten_tag_window_height')
      let g:guten_tag_window_height = 30
      endif
      if !exists('g:gute_tag_window_width')
      let g:guten_tag_window_width = 35
      endif

      augroup PluginGutenTag
      autocmd!
      autocmd BufRead,BufNewFile * call guten_tag#SetTagsPath()
      augroup END

      " Commands
      command! GutenTagOpenTagWindow call guten_tag#CreateTagWindow()


      Now for the actual working code. The first goal is accomplished by walking up the directory tree until one of 'marker files' is found, and then using a file in that directory as tags file. Here's the relevant parts of the code:



      " Search up the directory tree until one of the file markers is found, and
      " then uses this directory as a place to keep and look for tags file in.
      function! guten_tag#SetTagsPath()
      " First check if we're in an directory or a file which is forbidden from
      " having local tags files.
      let l:path = expand('%:p')
      if s:ForbiddenLocations()
      let &l:tags = g:guten_tag_tags_when_forbidden
      return
      endif
      " Find a marker
      while 1
      let l:path = fnamemodify(l:path, ':h')
      if s:HasMarkers(l:path)
      break
      endif
      if l:path ==# '/'
      return
      endif
      endwhile
      " Make a tags option
      let l:toplevel_tags =
      for l:tagfile in g:guten_tag_tags_files
      call add(l:toplevel_tags, l:path . '/' . l:tagfile)
      endfor
      let &l:tags = join(l:toplevel_tags, ',')
      endfunction

      " Test if a file matches any of the forbidden locations.
      function! s:ForbiddenLocations()
      let l:path = expand('%:p')
      for l:pattern in g:guten_tag_forbidden_paths
      if l:path =~# l:pattern
      return 1
      endif
      endfor
      return 0
      endfunction

      " Test if there is a marker in a given directory.
      function! s:HasMarkers(dirpath)
      for l:marker in g:guten_tag_markers
      if !empty(globpath(a:dirpath, l:marker, 0, 1))
      return 1
      endif
      endfor
      return 0
      endfunction


      ForbiddenLocations thing is used to avoid placing tags files in certain places and falling back to global tags file in such cases.



      The second goal is much harder, but it boils down to parsing a tags file and then building the hierarchy of tags inside. The tags are first separated by file they appear in, and then the said hierarchy is imposed on each segment based on some tag fields.



      The main exported function for this is guten_tag#CreateTagWindow:



      " Create a new window and populate it with tag tree
      function! guten_tag#CreateTagWindow()
      let l:files = tagfiles()
      if len(l:files) == 0
      return
      endif
      let l:file = l:files[0]
      let l:tags = s:ParseFile(l:file)
      let l:content = s:TagWindowContent(l:tags)
      let l:window_name = s:MakeWindowName(l:file)
      if bufexists(l:window_name)
      return
      endif
      if g:guten_tag_split_style ==# 'vertical'
      let l:command = g:guten_tag_window_width . 'vnew "' . l:window_name . '"'
      else
      let l:command = g:guten_tag_window_height . 'new "' . l:window_name . '"'
      endif
      exec l:command
      call append(0, l:content)
      setlocal buftype=nofile
      setlocal nomodifiable
      exec "file! " . l:window_name
      endfunction


      And here's the rest of the helpers, hopefully they are self-explanatory. If not, please say so, I'll add more detail then.



      " Add a line describing a tag to a list of lines
      function! s:AddLine(add_to, tag, indent_level, print_parent)
      let l:kind = s:GetKind(a:tag)
      let l:new_line = l:kind ==# '' ? '' : l:kind . ' '
      let l:new_line = l:new_line . a:tag.name
      if a:print_parent
      let l:parent = s:GetParentName(a:tag)
      if l:parent isnot# v:null
      let l:new_line = l:new_line . ' (owned by ' . (l:parent) . ')'
      endif
      endif
      let l:new_line = repeat(' ', a:indent_level) . l:new_line
      call add(a:add_to, l:new_line)
      call add(a:add_to, "")
      endfunction

      " Transform a flat list of tags into a dictionary where keys are file names
      " and values are lists of tags appearing in this file.
      function! s:BuildHierarchy(tags)
      let l:res =
      " First, extract top-level tags
      for l:tag in a:tags
      let l:parent = s:GetParentName(l:tag)
      if l:parent is# v:null
      call add(l:res, l:tag)
      endif
      endfor
      " Now, attach non-top-level tags to them
      for l:tags_in_file in values(s:SeparateTagsByFilename(a:tags))
      for l:tag in l:tags_in_file
      let l:parent_name = s:GetParentName(l:tag)
      if l:parent_name is# v:null
      continue
      endif
      let l:parent = s:TagByName(l:parent_name, l:tags_in_file)
      if l:parent is# v:null
      " If it's not possible to determine tag's parent, treat it as a top
      " level tag.
      call add(l:res, l:tag)
      continue
      endif
      call add(l:parent.children, l:tag)
      endfor
      endfor
      return s:SeparateTagsByFilename(l:res)
      endfunction

      " Get the name of a tag's parent, return v:null if there isn't one.
      function! s:GetParentName(tag)
      let l:fields = a:tag.fields
      let l:fields_to_test = ['struct', 'class']
      for l:fname in l:fields_to_test
      let l:res = get(l:fields, l:fname, v:null)
      if l:res isnot# v:null
      return l:res
      endif
      endfor
      return v:null
      endfunction

      " Get the first letter of 'kind' attribute
      function! s:GetKind(tag)
      if has_key(a:tag.fields, 'kind')
      let l:kind = a:tag.fields.kind
      else
      return ""
      endif
      if l:kind ==# ""
      return ""
      else
      return strcharpart(l:kind, 0, 1)
      endif
      endfunction

      " Test if a tag should not appear in the hierarchy of tags
      function! s:IgnoreTag(tag)
      let l:ignore_kinds = ['i', 'import', 'include', 'namespace']
      if !has_key(a:tag.fields, 'kind') || index(l:ignore_kinds, a:tag.fields.kind) ==# -1
      return 0
      else
      return 1
      endif
      endfunction

      " Generate a name for tag exploration window
      function! s:MakeWindowName(tags_file)
      return 'Guten Tag ' . fnamemodify(a:tags_file, ':p')
      endfunction

      " Parse a tag file into a sequence of top-level tags
      function! s:ParseFile(file)
      try
      let l:lines = readfile(a:file)
      catch /E484/
      return
      endtry
      " Read all tags
      let l:all_tags =
      for l:line in l:lines
      try
      let l:new_tag = s:ParseLine(l:line)
      catch /Comment line/
      continue
      endtry
      if !s:IgnoreTag(l:new_tag)
      call add(l:all_tags, l:new_tag)
      endif
      endfor
      return s:BuildHierarchy(uniq(sort(l:all_tags)))
      endfunction

      " Parse a tag file line into a dict.
      function! s:ParseLine(line)
      if a:line =~# 'v^!.*'
      throw 'Comment line'
      endif
      let l:extract_name_and_file = split(a:line, "t")
      let l:res =
      let l:res.name = l:extract_name_and_file[0]
      let l:res.filename = l:extract_name_and_file[1]
      let l:res.children =
      let l:rest = join(l:extract_name_and_file[2:], "t")
      " Extract EX search command
      let l:extract_fields = split(l:rest, '"')
      if len(l:extract_fields) <= 1
      res.fields =
      return res
      endif
      let l:res.search_cmd = l:extract_fields[0]
      " Just in case the fields contain a '"', join them back
      let l:extract_fields = join(l:extract_fields[1:], '"')
      let l:fields =
      for l:pair in split(l:extract_fields, "t")
      let l:split = split(l:pair, ':')
      if len(l:split) == 1
      let l:key = 'kind'
      let l:value = l:split[0]
      else
      let l:key = l:split[0]
      let l:value = join(l:split[1:], ':')
      endif
      let l:fields[l:key] = l:value
      endfor
      let l:res.fields = l:fields
      call s:TagPostprocessing(l:res)
      return res
      endfunction

      " Return a dict where keys are filenames and values are lists of tags
      " appearing in the corresponding file
      function! s:SeparateTagsByFilename(tags)
      let l:res =
      for l:tag in a:tags
      let l:file = l:tag.filename
      if has_key(l:res, l:file)
      call add(l:res[l:file], l:tag)
      else
      let l:res[l:file] = [l:tag]
      endif
      endfor
      return l:res
      endfunction

      " Return the first tag with a given name from a list, or null if there's no
      " such tag
      function! s:TagByName(name, list)
      for l:tag in a:list
      if l:tag.name ==# a:name
      return l:tag
      endif
      endfor
      return v:null
      endfunction

      " Compare two tags by name
      function! s:TagComparator(tag1, tag2)
      let l:name1 = a:tag1.name
      let l:name2 = a:tag2.name
      if l:name1 ># l:name2
      return 1
      elseif l:name1 ==# l:name2
      return 0
      else
      return -1
      endif
      endfunction

      " Return a list of lines in the tag overview window
      function! s:TagWindowContent(tags_hierarchy)
      let l:res =
      for l:file in keys(a:tags_hierarchy)
      call add(l:res, fnamemodify(l:file, ':p'))
      call add(l:res, "")
      for l:toplevel in a:tags_hierarchy[l:file]
      let l:indent = g:guten_tag_indent
      call s:AddLine(l:res, l:toplevel, l:indent, 1)
      let l:traversal = [[l:toplevel, 0]] " tag, current child index
      let l:indent += g:guten_tag_indent
      while len(l:traversal) ># 0
      let [l:parent, l:child_index] = l:traversal[-1]
      if l:child_index ==# len(l:parent.children)
      unlet l:traversal[-1]
      let l:indent -= g:guten_tag_indent
      continue
      endif
      let l:cur = l:parent.children[l:child_index]
      call s:AddLine(l:res, l:cur, l:indent, 0)
      if len(l:cur.children) > 0
      call add(l:traversal, [l:cur, 0])
      let l:indent += g:guten_tag_indent
      echomsg l:indent
      continue
      endif
      let l:traversal[-1][1] += 1
      endwhile
      endfor
      endfor
      return l:res
      endfunction

      " Do some actions after all fields and attributes of a tag were read
      function! s:TagPostprocessing(tag)
      " Presently, do nothing. TODO: language-specific processing, like function
      " signature extraction
      endfunction








      share|improve this question












      share|improve this question




      share|improve this question








      edited May 26 at 15:01
























      asked May 26 at 3:58









      Michail

      2648




      2648

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f195200%2fa-vim-plugin-to-manage-and-view-tag-files%23new-answer', 'question_page');

          );

          Post as a guest



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f195200%2fa-vim-plugin-to-manage-and-view-tag-files%23new-answer', 'question_page');

          );

          Post as a guest













































































          Popular posts from this blog

          Python Lists

          Aion

          JavaScript Array Iteration Methods