bb40826def
Instead of forcing the TOC to always be at header level 1, allow the user to specify via the option g:vimwiki_toc_header_level what level they want. This defaults to 1, so if the user does nothing then the old behavior will remain the same.
343 lines
11 KiB
VimL
343 lines
11 KiB
VimL
" vim:tabstop=2:shiftwidth=2:expandtab:textwidth=99
|
|
" Vimwiki autoload plugin file
|
|
|
|
|
|
let s:TAGS_METADATA_FILE_NAME = '.tags'
|
|
|
|
|
|
|
|
" Tags metadata in-memory format:
|
|
" metadata := { 'pagename': [entries, ...] }
|
|
" entry := { 'tagname':..., 'lineno':..., 'link':... }
|
|
|
|
" Tags metadata in-file format:
|
|
"
|
|
" Is based on CTags format (see |tags-file-format|).
|
|
"
|
|
" {tagaddress} is set to lineno. We'll let vim search by exact line number; we
|
|
" can afford that, we assume metadata file is always updated before use.
|
|
"
|
|
" Pagename and link are not saved in standard ctags fields, so we'll add
|
|
" an optional field, "vimwiki:". In this field, we encode tab-separated values
|
|
" of missing parameters -- "pagename" and "link".
|
|
|
|
|
|
|
|
" Update tags metadata.
|
|
" a:full_rebuild == 1: re-scan entire wiki
|
|
" a:full_rebuild == 0: only re-scan current page
|
|
" a:all_files == '': only if the file is newer than .tags
|
|
function! vimwiki#tags#update_tags(full_rebuild, all_files)
|
|
let all_files = a:all_files != ''
|
|
if !a:full_rebuild
|
|
" Updating for one page (current)
|
|
let page_name = vimwiki#vars#get_bufferlocal('subdir') . expand('%:t:r')
|
|
" Collect tags in current file
|
|
let tags = s:scan_tags(getline(1, '$'), page_name)
|
|
" Load metadata file
|
|
let metadata = s:load_tags_metadata()
|
|
" Drop old tags
|
|
let metadata = s:remove_page_from_tags(metadata, page_name)
|
|
" Merge in the new ones
|
|
let metadata = s:merge_tags(metadata, page_name, tags)
|
|
" Save
|
|
call s:write_tags_metadata(metadata)
|
|
else " full rebuild
|
|
let files = vimwiki#base#find_files(vimwiki#vars#get_bufferlocal('wiki_nr'), 0)
|
|
let wiki_base_dir = vimwiki#vars#get_wikilocal('path')
|
|
let tags_file_last_modification = getftime(vimwiki#tags#metadata_file_path())
|
|
let metadata = s:load_tags_metadata()
|
|
for file in files
|
|
if all_files || getftime(file) >= tags_file_last_modification
|
|
let subdir = vimwiki#base#subdir(wiki_base_dir, file)
|
|
let page_name = subdir . fnamemodify(file, ':t:r')
|
|
let tags = s:scan_tags(readfile(file), page_name)
|
|
let metadata = s:remove_page_from_tags(metadata, page_name)
|
|
let metadata = s:merge_tags(metadata, page_name, tags)
|
|
endif
|
|
endfor
|
|
call s:write_tags_metadata(metadata)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Scans the list of text lines (argument) and produces tags metadata as a list of tag entries.
|
|
function! s:scan_tags(lines, page_name)
|
|
|
|
let entries = []
|
|
|
|
" Code wireframe to scan for headers -- borrowed from
|
|
" vimwiki#base#get_anchors(), with minor modifications.
|
|
|
|
let rxheader = vimwiki#vars#get_syntaxlocal('header_search')
|
|
let rxtag = vimwiki#vars#get_syntaxlocal('tag_search')
|
|
|
|
let anchor_level = ['', '', '', '', '', '', '']
|
|
let current_complete_anchor = ''
|
|
|
|
let PROXIMITY_LINES_NR = 2
|
|
let header_line_nr = - (2 * PROXIMITY_LINES_NR)
|
|
|
|
for line_nr in range(1, len(a:lines))
|
|
let line = a:lines[line_nr - 1]
|
|
|
|
" process headers
|
|
let h_match = matchlist(line, rxheader)
|
|
if !empty(h_match) " got a header
|
|
let header_line_nr = line_nr
|
|
let header = vimwiki#u#trim(h_match[2])
|
|
let level = len(h_match[1])
|
|
let anchor_level[level-1] = header
|
|
for l in range(level, 6)
|
|
let anchor_level[l] = ''
|
|
endfor
|
|
if level == 1
|
|
let current_complete_anchor = header
|
|
else
|
|
let current_complete_anchor = ''
|
|
for l in range(level-1)
|
|
if anchor_level[l] != ''
|
|
let current_complete_anchor .= anchor_level[l].'#'
|
|
endif
|
|
endfor
|
|
let current_complete_anchor .= header
|
|
endif
|
|
continue " tags are not allowed in headers
|
|
endif
|
|
|
|
" TODO ignore verbatim blocks
|
|
|
|
" Scan line for tags. There can be many of them.
|
|
let str = line
|
|
while 1
|
|
let tag_group = matchstr(str, rxtag)
|
|
if tag_group == ''
|
|
break
|
|
endif
|
|
let tagend = matchend(str, rxtag)
|
|
let str = str[(tagend):]
|
|
for tag in split(tag_group, ':')
|
|
" Create metadata entry
|
|
let entry = {}
|
|
let entry.tagname = tag
|
|
let entry.lineno = line_nr
|
|
if line_nr <= PROXIMITY_LINES_NR && header_line_nr < 0
|
|
" Tag appeared at the top of the file
|
|
let entry.link = a:page_name
|
|
elseif line_nr <= (header_line_nr + PROXIMITY_LINES_NR)
|
|
" Tag appeared right below a header
|
|
let entry.link = a:page_name . '#' . current_complete_anchor
|
|
else
|
|
" Tag stands on its own
|
|
let entry.link = a:page_name . '#' . tag
|
|
endif
|
|
call add(entries, entry)
|
|
endfor
|
|
endwhile
|
|
|
|
endfor " loop over lines
|
|
return entries
|
|
endfunction
|
|
|
|
|
|
" Returns tags metadata file path
|
|
function! vimwiki#tags#metadata_file_path() abort
|
|
return fnamemodify(vimwiki#path#join_path(vimwiki#vars#get_wikilocal('path'),
|
|
\ s:TAGS_METADATA_FILE_NAME), ':p')
|
|
endfunction
|
|
|
|
|
|
" Loads tags metadata from file, returns a dictionary
|
|
function! s:load_tags_metadata() abort
|
|
let metadata_path = vimwiki#tags#metadata_file_path()
|
|
if !filereadable(metadata_path)
|
|
return {}
|
|
endif
|
|
let metadata = {}
|
|
for line in readfile(metadata_path)
|
|
if line =~ '^!_TAG_FILE_'
|
|
continue
|
|
endif
|
|
let parts = matchlist(line, '^\(.\{-}\);"\(.*\)$')
|
|
if parts[0] == '' || parts[1] == '' || parts[2] == ''
|
|
throw 'VimwikiTags1: Metadata file corrupted'
|
|
endif
|
|
let std_fields = split(parts[1], '\t')
|
|
if len(std_fields) != 3
|
|
throw 'VimwikiTags2: Metadata file corrupted'
|
|
endif
|
|
let vw_part = parts[2]
|
|
if vw_part[0] != "\t"
|
|
throw 'VimwikiTags3: Metadata file corrupted'
|
|
endif
|
|
let vw_fields = split(vw_part[1:], "\t")
|
|
if len(vw_fields) != 1 || vw_fields[0] !~ '^vimwiki:'
|
|
throw 'VimwikiTags4: Metadata file corrupted'
|
|
endif
|
|
let vw_data = substitute(vw_fields[0], '^vimwiki:', '', '')
|
|
let vw_data = substitute(vw_data, '\\n', "\n", 'g')
|
|
let vw_data = substitute(vw_data, '\\r', "\r", 'g')
|
|
let vw_data = substitute(vw_data, '\\t', "\t", 'g')
|
|
let vw_data = substitute(vw_data, '\\\\', "\\", 'g')
|
|
let vw_fields = split(vw_data, "\t")
|
|
if len(vw_fields) != 2
|
|
throw 'VimwikiTags5: Metadata file corrupted'
|
|
endif
|
|
let pagename = vw_fields[0]
|
|
let entry = {}
|
|
let entry.tagname = std_fields[0]
|
|
let entry.lineno = std_fields[2]
|
|
let entry.link = vw_fields[1]
|
|
if has_key(metadata, pagename)
|
|
call add(metadata[pagename], entry)
|
|
else
|
|
let metadata[pagename] = [entry]
|
|
endif
|
|
endfor
|
|
return metadata
|
|
endfunction
|
|
|
|
|
|
" Removes all entries for given page from metadata in-place. Returns updated
|
|
" metadata (just in case).
|
|
function! s:remove_page_from_tags(metadata, page_name)
|
|
if has_key(a:metadata, a:page_name)
|
|
call remove(a:metadata, a:page_name)
|
|
return a:metadata
|
|
else
|
|
return a:metadata
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Merges metadata of one file into a:metadata
|
|
function! s:merge_tags(metadata, pagename, file_metadata)
|
|
let metadata = a:metadata
|
|
let metadata[a:pagename] = a:file_metadata
|
|
return metadata
|
|
endfunction
|
|
|
|
|
|
" Compares two actual lines from tags file. Return value is in strcmp style.
|
|
" See help on sort() -- that's what this function is going to be used for.
|
|
" See also s:write_tags_metadata below -- that's where we compose these tags
|
|
" file lines.
|
|
"
|
|
" This function is needed for tags sorting, since plain sort() compares line
|
|
" numbers as strings, not integers, and so, for example, tag at line 14
|
|
" preceeds the same tag on the same page at line 9. (Because string "14" is
|
|
" alphabetically 'less than' string "9".)
|
|
function! s:tags_entry_cmp(i1, i2)
|
|
let items = []
|
|
for orig_item in [a:i1, a:i2]
|
|
let fields = split(orig_item, "\t")
|
|
let item = {}
|
|
let item.text = fields[0]."\t".fields[1]
|
|
let item.lineno = 0 + matchstr(fields[2], '\m\d\+')
|
|
call add(items, item)
|
|
endfor
|
|
if items[0].text ># items[1].text
|
|
return 1
|
|
elseif items[0].text <# items[1].text
|
|
return -1
|
|
elseif items[0].lineno > items[1].lineno
|
|
return 1
|
|
elseif items[0].lineno < items[1].lineno
|
|
return -1
|
|
else
|
|
return 0
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Saves metadata object into a file. Throws exceptions in case of problems.
|
|
function! s:write_tags_metadata(metadata)
|
|
let metadata_path = vimwiki#tags#metadata_file_path()
|
|
let tags = []
|
|
for pagename in keys(a:metadata)
|
|
for entry in a:metadata[pagename]
|
|
let entry_data = pagename . "\t" . entry.link
|
|
let entry_data = substitute(entry_data, "\\", '\\\\', 'g')
|
|
let entry_data = substitute(entry_data, "\t", '\\t', 'g')
|
|
let entry_data = substitute(entry_data, "\r", '\\r', 'g')
|
|
let entry_data = substitute(entry_data, "\n", '\\n', 'g')
|
|
call add(tags,
|
|
\ entry.tagname . "\t"
|
|
\ . pagename . vimwiki#vars#get_wikilocal('ext') . "\t"
|
|
\ . entry.lineno
|
|
\ . ';"'
|
|
\ . "\t" . "vimwiki:" . entry_data
|
|
\)
|
|
endfor
|
|
endfor
|
|
call sort(tags, "s:tags_entry_cmp")
|
|
call insert(tags, "!_TAG_FILE_SORTED\t1\t")
|
|
call writefile(tags, metadata_path)
|
|
endfunction
|
|
|
|
|
|
" Returns list of unique tags found in the .tags file
|
|
function! vimwiki#tags#get_tags()
|
|
let metadata = s:load_tags_metadata()
|
|
let tags = {}
|
|
for entries in values(metadata)
|
|
for entry in entries
|
|
let tags[entry.tagname] = 1
|
|
endfor
|
|
endfor
|
|
return keys(tags)
|
|
endfunction
|
|
|
|
|
|
" Similar to vimwiki#base#generate_links. In the current buffer, appends
|
|
" tags and references to all their instances. If no arguments (tags) are
|
|
" specified, outputs all tags.
|
|
function! vimwiki#tags#generate_tags(...) abort
|
|
let need_all_tags = (a:0 == 0)
|
|
let specific_tags = a:000
|
|
|
|
let metadata = s:load_tags_metadata()
|
|
|
|
" make a dictionary { tag_name: [tag_links, ...] }
|
|
let tags_entries = {}
|
|
for entries in values(metadata)
|
|
for entry in entries
|
|
if has_key(tags_entries, entry.tagname)
|
|
call add(tags_entries[entry.tagname], entry.link)
|
|
else
|
|
let tags_entries[entry.tagname] = [entry.link]
|
|
endif
|
|
endfor
|
|
endfor
|
|
|
|
let lines = []
|
|
let bullet = repeat(' ', vimwiki#lst#get_list_margin()).vimwiki#lst#default_symbol().' '
|
|
for tagname in sort(keys(tags_entries))
|
|
if need_all_tags || index(specific_tags, tagname) != -1
|
|
call extend(lines, [
|
|
\ '',
|
|
\ substitute(vimwiki#vars#get_syntaxlocal('rxH2_Template'), '__Header__', tagname, ''),
|
|
\ '' ])
|
|
for taglink in sort(tags_entries[tagname])
|
|
call add(lines, bullet . substitute(vimwiki#vars#get_global('WikiLinkTemplate1'),
|
|
\ '__LinkUrl__', taglink, ''))
|
|
endfor
|
|
endif
|
|
endfor
|
|
|
|
let links_rx = '\m\%(^\s*$\)\|\%('.vimwiki#vars#get_syntaxlocal('rxH2').'\)\|\%(^\s*'
|
|
\ .vimwiki#u#escape(vimwiki#lst#default_symbol()).' '
|
|
\ .vimwiki#vars#get_syntaxlocal('rxWikiLink').'$\)'
|
|
|
|
call vimwiki#base#update_listing_in_buffer(lines, 'Generated Tags', links_rx, line('$')+1, 1, 1)
|
|
endfunction
|
|
|
|
|
|
function! vimwiki#tags#complete_tags(ArgLead, CmdLine, CursorPos) abort
|
|
" We can safely ignore args if we use -custom=complete option, Vim engine
|
|
" will do the job of filtering.
|
|
let taglist = vimwiki#tags#get_tags()
|
|
return join(taglist, "\n")
|
|
endfunction
|
|
|