diff --git a/autoload/vimwiki/base.vim b/autoload/vimwiki/base.vim index a350193..609a77f 100644 --- a/autoload/vimwiki/base.vim +++ b/autoload/vimwiki/base.vim @@ -465,10 +465,9 @@ endfunction " }}} " vimwiki#base#generate_links function! vimwiki#base#generate_links() "{{{ + let lines = [] + let links = vimwiki#base#get_wikilinks(g:vimwiki_current_idx, 0) - - call append(line('$'), substitute(g:vimwiki_rxH1_Template, '__Header__', 'Generated Links', '')) - call sort(links) let bullet = repeat(' ', vimwiki#lst#get_list_margin()). @@ -476,10 +475,15 @@ function! vimwiki#base#generate_links() "{{{ for link in links let abs_filepath = vimwiki#path#abs_path_of_link(link) if !s:is_diary_file(abs_filepath) - call append(line('$'), bullet. + call add(lines, bullet. \ substitute(g:vimwiki_WikiLinkTemplate1, '__LinkUrl__', '\='."'".link."'", '')) endif endfor + + let links_rx = '\m^\s*'.vimwiki#u#escape(vimwiki#lst#default_symbol()).' ' + + call vimwiki#base#update_listing_in_buffer(lines, 'Generated Links', links_rx, + \ line('$')+1, 1) endfunction " }}} " vimwiki#base#goto @@ -498,7 +502,7 @@ function! vimwiki#base#backlinks() "{{{ let locations = [] for idx in range(len(g:vimwiki_list)) let syntax = VimwikiGet('syntax', idx) - let wikifiles = s:find_files(idx, 0) + let wikifiles = vimwiki#base#find_files(idx, 0) for source_file in wikifiles let links = s:get_links(source_file, idx) for [target_file, _, lnum, col] in links @@ -522,7 +526,7 @@ endfunction "}}} " Returns: a list containing all files of the given wiki as absolute file path. " If the given wiki number is negative, the diary of the current wiki is used " If the second argument is not zero, only directories are found -function! s:find_files(wiki_nr, directories_only) +function! vimwiki#base#find_files(wiki_nr, directories_only) let wiki_nr = a:wiki_nr if wiki_nr >= 0 let root_directory = VimwikiGet('path', wiki_nr) @@ -551,7 +555,7 @@ endfunction " If the given wiki number is negative, the diary of the current wiki is used. " If also_absolute_links is nonzero, also return links of the form /file function! vimwiki#base#get_wikilinks(wiki_nr, also_absolute_links) - let files = s:find_files(a:wiki_nr, 0) + let files = vimwiki#base#find_files(a:wiki_nr, 0) if a:wiki_nr == g:vimwiki_current_idx let cwd = vimwiki#path#wikify_path(expand('%:p:h')) elseif a:wiki_nr < 0 @@ -582,7 +586,7 @@ endfunction " Returns: a list containing the links to all directories from the current file function! vimwiki#base#get_wiki_directories(wiki_nr) - let dirs = s:find_files(a:wiki_nr, 1) + let dirs = vimwiki#base#find_files(a:wiki_nr, 1) if a:wiki_nr == g:vimwiki_current_idx let cwd = vimwiki#path#wikify_path(expand('%:p:h')) let root_dir = VimwikiGet('path') @@ -608,6 +612,7 @@ function! vimwiki#base#get_anchors(filename, syntax) "{{{ let rxheader = g:vimwiki_{a:syntax}_header_search let rxbold = g:vimwiki_{a:syntax}_bold_search + let rxtag = g:vimwiki_{a:syntax}_tag_search let anchor_level = ['', '', '', '', '', '', ''] let anchors = [] @@ -652,6 +657,22 @@ function! vimwiki#base#get_anchors(filename, syntax) "{{{ let bold_count += 1 endwhile + " collect tags text (there can be several in one line) + let tag_count = 1 + while 1 + let tag_group_text = matchstr(line, rxtag, 0, tag_count) + if tag_group_text == '' + break + endif + for tag_text in split(tag_group_text, ':') + call add(anchors, tag_text) + if current_complete_anchor != '' + call add(anchors, current_complete_anchor.'#'.tag_text) + endif + endfor + let tag_count += 1 + endwhile + endfor return anchors @@ -672,8 +693,12 @@ function! s:jump_to_anchor(anchor) "{{{ \ '__Header__', "\\='".segment."'", '') let anchor_bold = substitute(g:vimwiki_{VimwikiGet('syntax')}_bold_match, \ '__Text__', "\\='".segment."'", '') + let anchor_tag = substitute(g:vimwiki_{VimwikiGet('syntax')}_tag_match, + \ '__Tag__', "\\='".segment."'", '') - if !search(anchor_header, 'Wc') && !search(anchor_bold, 'Wc') + if !search(anchor_tag, 'Wc') + \ && !search(anchor_header, 'Wc') + \ && !search(anchor_bold, 'Wc') call setpos('.', oldpos) break endif @@ -723,7 +748,7 @@ function! vimwiki#base#check_links() "{{{ let errors = [] for idx in range(len(g:vimwiki_list)) let syntax = VimwikiGet('syntax', idx) - let wikifiles = s:find_files(idx, 0) + let wikifiles = vimwiki#base#find_files(idx, 0) for wikifile in wikifiles let links_of_files[wikifile] = s:get_links(wikifile, idx) let anchors_of_files[wikifile] = vimwiki#base#get_anchors(wikifile, syntax) @@ -1066,6 +1091,74 @@ function! vimwiki#base#nested_syntax(filetype, start, end, textSnipHl) abort "{{ endif endfunction "}}} +" creates or updates auto-generated listings in a wiki file, like TOC, diary +" links, tags list etc. +" - the listing consists of a level 1 header and a list of strings as content +" - a:content_regex is used to determine how long a potentially existing list is +" - a:default_lnum is the line number where the new listing should be placed if +" it's not already present +" - if a:create is true, it will be created if it doesn't exist, otherwise it +" will only be updated if it already exists +function! vimwiki#base#update_listing_in_buffer(strings, start_header, + \ content_regex, default_lnum, create) "{{{ + " apparently, Vim behaves strange when files change while in diff mode + if &diff || &readonly + return + endif + + " check if the listing is already there + let already_there = 0 + + let header_rx = '\m^\s*'. + \ substitute(g:vimwiki_rxH1_Template, '__Header__', a:start_header, '') + \ .'\s*$' + + let start_lnum = 1 + while start_lnum <= line('$') + if getline(start_lnum) =~# header_rx + let already_there = 1 + break + endif + let start_lnum += 1 + endwhile + + if !already_there && !a:create + return + endif + + let old_cursor_pos = getpos('.') + + if already_there + " delete the old listing + let whitespaces_in_first_line = matchstr(getline(start_lnum), '\m^\s*') + let end_lnum = start_lnum + 1 + while end_lnum <= line('$') && getline(end_lnum) =~# a:content_regex + let end_lnum += 1 + endwhile + silent exe start_lnum.','.string(end_lnum - 1).'delete _' + else + let start_lnum = a:default_lnum + let whitespaces_in_first_line = '' + endif + + " write new listing + let new_header = whitespaces_in_first_line + \ . substitute(g:vimwiki_rxH1_Template, + \ '__Header__', '\='."'".a:start_header."'", '') + call append(start_lnum - 1, new_header) + let start_lnum += 1 + for string in a:strings + call append(start_lnum - 1, string) + let start_lnum += 1 + endfor + " append an empty line if there is not one + if start_lnum <= line('$') && getline(start_lnum) !~# '\m^\s*$' + call append(start_lnum - 1, '') + endif + + call setpos('.', old_cursor_pos) +endfunction "}}} + " WIKI link following functions {{{ " vimwiki#base#find_next_link function! vimwiki#base#find_next_link() "{{{ @@ -1143,6 +1236,9 @@ function! vimwiki#base#go_back_link() "{{{ let prev_word = b:vimwiki_prev_link execute ":e ".substitute(prev_word[0], '\s', '\\\0', 'g') call setpos('.', prev_word[1]) + else + " maybe we came here by jumping to a tag -> pop from the tag stack + silent! pop! endif endfunction " }}} @@ -1606,44 +1702,6 @@ endfunction " }}} " a:create == 1: creates or updates TOC in current file " a:create == 0: update if TOC exists function! vimwiki#base#table_of_contents(create) - " apparently, Vim behaves strange when files change while in diff mode - if &diff - return - endif - - " look for existing TOC - let toc_header = '^\s*'.substitute(g:vimwiki_rxH1_Template, '__Header__', - \ '\='."'".g:vimwiki_toc_header."'", '').'\s*$' - let toc_line = 0 - let lnum = 1 - while lnum <= &modelines + 2 && lnum <= line('$') - if getline(lnum) =~# toc_header - let toc_line = lnum - break - endif - let lnum += 1 - endwhile - - if !a:create && toc_line <= 0 - return - endif - - let old_cursor_pos = getpos('.') - let bullet = vimwiki#lst#default_symbol().' ' - let rx_bullet = vimwiki#u#escape(bullet) - let whitespaces = matchstr(getline(toc_line), '^\s*') - - " delete old TOC - if toc_line > 0 - let endoftoc = toc_line+1 - while endoftoc <= line('$') && getline(endoftoc) =~# '^\s*'.rx_bullet.g:vimwiki_rxWikiLink.'\s*$' - let endoftoc += 1 - endwhile - silent exe toc_line.','.string(endoftoc-1).'delete _' - else - let toc_line = 1 - endif - " collect new headers let headers = [] let headers_levels = [['', 0], ['', 0], ['', 0], ['', 0], ['', 0], ['', 0]] @@ -1654,6 +1712,9 @@ function! vimwiki#base#table_of_contents(create) endif let h_level = vimwiki#u#count_first_sym(line_content) let h_text = vimwiki#u#trim(matchstr(line_content, g:vimwiki_rxHeader)) + if h_text ==# g:vimwiki_toc_header " don't include the TOC's header itself + continue + endif let headers_levels[h_level-1] = [h_text, headers_levels[h_level-1][1]+1] for idx in range(h_level, 5) | let headers_levels[idx] = ['', 0] | endfor @@ -1676,25 +1737,23 @@ function! vimwiki#base#table_of_contents(create) call add(headers, [h_level, h_complete_id, h_text]) endfor - " write new TOC - call append(toc_line-1, whitespaces . substitute(g:vimwiki_rxH1_Template, - \ '__Header__', '\='."'".g:vimwiki_toc_header."'", '')) - + let lines = [] let startindent = repeat(' ', vimwiki#lst#get_list_margin()) let indentstring = repeat(' ', vimwiki#u#sw()) + let bullet = vimwiki#lst#default_symbol().' ' for [lvl, link, desc] in headers let esc_link = substitute(link, "'", "''", 'g') let esc_desc = substitute(desc, "'", "''", 'g') let link = substitute(g:vimwiki_WikiLinkTemplate2, '__LinkUrl__', \ '\='."'".'#'.esc_link."'", '') let link = substitute(link, '__LinkDescription__', '\='."'".esc_desc."'", '') - call append(toc_line, startindent.repeat(indentstring, lvl-1).bullet.link) - let toc_line += 1 + call add(lines, startindent.repeat(indentstring, lvl-1).bullet.link) endfor - if getline(toc_line+1) !~# '^\s*$' - call append(toc_line, '') - endif - call setpos('.', old_cursor_pos) + + let links_rx = '\m^\s*'.vimwiki#u#escape(vimwiki#lst#default_symbol()).' ' + + call vimwiki#base#update_listing_in_buffer(lines, g:vimwiki_toc_header, links_rx, + \ 1, a:create) endfunction "}}} diff --git a/autoload/vimwiki/diary.vim b/autoload/vimwiki/diary.vim index d131efe..540a766 100644 --- a/autoload/vimwiki/diary.vim +++ b/autoload/vimwiki/diary.vim @@ -121,36 +121,31 @@ fun! s:group_links(links) "{{{ return result endfun "}}} -fun! s:sort(lst) "{{{ +function! s:sort(lst) "{{{ if VimwikiGet("diary_sort") ==? 'desc' return reverse(sort(a:lst)) else return sort(a:lst) endif -endfun "}}} +endfunction "}}} -fun! s:format_diary(...) "{{{ +function! s:format_diary(...) "{{{ let result = [] - call add(result, substitute(g:vimwiki_rxH1_Template, '__Header__', VimwikiGet('diary_header'), '')) - if a:0 let g_files = s:group_links(s:get_diary_links(a:1)) else let g_files = s:group_links(s:get_diary_links()) endif - " for year in s:rev(sort(keys(g_files))) for year in s:sort(keys(g_files)) call add(result, '') call add(result, substitute(g:vimwiki_rxH2_Template, '__Header__', year , '')) - " for month in s:rev(sort(keys(g_files[year]))) for month in s:sort(keys(g_files[year])) call add(result, '') call add(result, substitute(g:vimwiki_rxH3_Template, '__Header__', s:get_month_name(month), '')) - " for [fl, cap] in s:rev(sort(items(g_files[year][month]))) for [fl, cap] in s:sort(items(g_files[year][month])) if empty(cap) let entry = substitute(g:vimwiki_WikiLinkTemplate1, '__LinkUrl__', fl, '') @@ -165,46 +160,8 @@ fun! s:format_diary(...) "{{{ endfor endfor - call add(result, '') return result -endfun "}}} - -function! s:delete_diary_section() "{{{ - " remove diary section - let old_pos = getpos('.') - let ln_start = -1 - let ln_end = -1 - call cursor(1, 1) - if search(substitute(g:vimwiki_rxH1_Template, '__Header__', VimwikiGet('diary_header'), ''), 'Wc') - let ln_start = line('.') - if search(g:vimwiki_rxH1, 'W') - let ln_end = line('.') - 1 - else - let ln_end = line('$') - endif - endif - - if ln_start < 0 || ln_end < 0 - call setpos('.', old_pos) - return - endif - - if !&readonly - exe ln_start.",".ln_end."delete _" - endif - - call setpos('.', old_pos) -endfunction "}}} - -function! s:insert_diary_section() "{{{ - if !&readonly - let ln = line('.') - call append(ln, s:format_diary()) - if ln == 1 && getline(ln) == '' - 1,1delete - endif - endif endfunction "}}} " Diary index stuff }}} @@ -300,8 +257,9 @@ function! vimwiki#diary#generate_diary_section() "{{{ let current_file = vimwiki#path#path_norm(expand("%:p")) let diary_file = vimwiki#path#path_norm(s:diary_index()) if vimwiki#path#is_equal(current_file, diary_file) - call s:delete_diary_section() - call s:insert_diary_section() + let content_rx = '^\%(\s*\* \)\|\%(^\s*$\)\|\%('.g:vimwiki_rxHeader.'\)' + call vimwiki#base#update_listing_in_buffer(s:format_diary(), + \ VimwikiGet('diary_header'), content_rx, line('$')+1, 1) else echom "vimwiki: You can generate diary links only in a diary index page!" endif diff --git a/autoload/vimwiki/html.vim b/autoload/vimwiki/html.vim index a338acd..4c4a769 100644 --- a/autoload/vimwiki/html.vim +++ b/autoload/vimwiki/html.vim @@ -289,6 +289,27 @@ function! s:tag_strong(value, header_ids) "{{{ \ .id.'">'.text.'' endfunction "}}} +function! s:tag_tags(value, header_ids) "{{{ + let complete_id = '' + for level in range(6) + if a:header_ids[level][0] != '' + let complete_id .= a:header_ids[level][0].'-' + endif + endfor + if a:header_ids[5][0] == '' + let complete_id = complete_id[:-2] + endif + let complete_id = s:safe_html_anchor(complete_id) + + let result = [] + for tag in split(a:value, ':') + let id = s:safe_html_anchor(tag) + call add(result, ''.tag.'') + endfor + return join(result) +endfunction "}}} + function! s:tag_todo(value) "{{{ return ''.a:value.'' endfunction "}}} @@ -554,6 +575,7 @@ function! s:process_tags_typefaces(line, header_ids) "{{{ let line = s:make_tag(line, g:vimwiki_rxSubScript, 's:tag_sub') let line = s:make_tag(line, g:vimwiki_rxCode, 's:tag_code') let line = s:make_tag(line, g:vimwiki_rxEqIn, 's:tag_eqin') + let line = s:make_tag(line, g:vimwiki_rxTags, 's:tag_tags', a:header_ids) return line endfunction " }}} diff --git a/autoload/vimwiki/style.css b/autoload/vimwiki/style.css index 752867b..b9d1c02 100644 --- a/autoload/vimwiki/style.css +++ b/autoload/vimwiki/style.css @@ -24,6 +24,7 @@ del {text-decoration: line-through; color: #777777;} .justright {text-align: right;} .justcenter {text-align: center;} .center {margin-left: auto; margin-right: auto;} +.tag {background-color: #eeeeee; font-family: monospace; padding: 2px;} /* classes for items of todo lists */ .done0 { diff --git a/autoload/vimwiki/tags.vim b/autoload/vimwiki/tags.vim new file mode 100644 index 0000000..4bd13a9 --- /dev/null +++ b/autoload/vimwiki/tags.vim @@ -0,0 +1,307 @@ +" vim:tabstop=2:shiftwidth=2:expandtab:foldmethod=marker:textwidth=79 +" 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". + +" vimwiki#tags#update_tags +" 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 = VimwikiGet('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(g:vimwiki_current_idx, 0) + 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 page_name = 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 " }}} + +" s:scan_tags +" 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 = [] + let page_name = a:page_name + + " Code wireframe to scan for headers -- borrowed from + " vimwiki#base#get_anchors(), with minor modifications. + + let rxheader = g:vimwiki_{VimwikiGet('syntax')}_header_search + let rxtag = g:vimwiki_{VimwikiGet('syntax')}_tag_search + + let anchor_level = ['', '', '', '', '', '', ''] + let current_complete_anchor = '' + + let PROXIMITY_LINES_NR = 5 + 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 = page_name + elseif line_nr <= (header_line_nr + PROXIMITY_LINES_NR) + let entry.link = page_name . '#' . current_complete_anchor + else + let entry.link = page_name . '#' . tag + endif + call add(entries, entry) + endfor + endwhile + + endfor " loop over lines + return entries +endfunction " }}} + +" vimwiki#tags#metadata_file_path +" Returns tags metadata file path +function! vimwiki#tags#metadata_file_path() abort "{{{ + return fnamemodify(VimwikiGet('path') . '/' . s:TAGS_METADATA_FILE_NAME, ':p') +endfunction " }}} + +" s:load_tags_metadata +" 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 " }}} + +" s:remove_page_from_tags +" 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 " }}} + +" s:merge_tags +" 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 " }}} + +" s:write_tags_metadata +" 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 . VimwikiGet('ext') . "\t" + \ . entry.lineno + \ . ';"' + \ . "\t" . "vimwiki:" . entry_data + \) + endfor + endfor + call sort(tags) + call insert(tags, "!_TAG_FILE_SORTED\t1\t") + call writefile(tags, metadata_path) +endfunction " }}} + +" vimwiki#tags#get_tags +" 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 " }}} + +" vimwiki#tags#generate_tags +" 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(g:vimwiki_rxH2_Template, '__Header__', tagname, ''), + \ '' ]) + for taglink in tags_entries[tagname] + call add(lines, bullet . + \ substitute(g:vimwiki_WikiLinkTemplate1, '__LinkUrl__', taglink, '')) + endfor + endif + endfor + + let links_rx = '\m\%(^\s*$\)\|\%('.g:vimwiki_rxH2.'\)\|\%(^\s*' + \ .vimwiki#u#escape(vimwiki#lst#default_symbol()).' ' + \ .g:vimwiki_rxWikiLink.'$\)' + + call vimwiki#base#update_listing_in_buffer(lines, 'Generated Tags', links_rx, + \ line('$')+1, 1) +endfunction " }}} + +" vimwiki#tags#complete_tags +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 " }}} + diff --git a/doc/vimwiki.txt b/doc/vimwiki.txt index d80012f..443ee1c 100644 --- a/doc/vimwiki.txt +++ b/doc/vimwiki.txt @@ -38,6 +38,7 @@ CONTENTS *vimwiki-contents* 5.12. Schemes |vimwiki-syntax-schemes| 5.13. Transclusions |vimwiki-syntax-transclude| 5.14. Thumbnails |vimwiki-syntax-thumbnails| + 5.15. Tags |vimwiki-syntax-tags| 6. Folding/Outline |vimwiki-folding| 7. Placeholders |vimwiki-placeholders| 8. Lists |vimwiki-lists| @@ -595,7 +596,7 @@ il A single list item. *:VimwikiGoto* Goto link provided by an argument. For example: > :VimwikiGoto HelloWorld -< opens opens/creates HelloWorld wiki page. +< opens opens/creates HelloWorld wiki page. Supports |cmdline-completion| for link name. @@ -703,6 +704,21 @@ il A single list item. files are reachable from the index file. Errors are shown in the quickfix window. +*:VimwikiRebuildTags* + Rebuilds the tags metadata file for the current wiki file. + Necessary for all tags related commands: |vimwiki-syntax-tags|. + + :VimwikiRebuildTags! does the same for all files in the current wiki. + +*:VimwikiSearchTags* + Searches over the pages in current wiki and finds all locations of a given + tag. Supports |cmdline-completion|. + +*:VimwikiGenerateTags* + Similar to |:VimwikiGenerateLinks|. In the current buffer, appends tags + and references to all their instances. Supports |cmdline-completion|. If + no arguments (tags) are specified, outputs all tags. + ============================================================================== 5. Wiki syntax *vimwiki-syntax* @@ -1227,6 +1243,45 @@ in HTML: > + :ind +which opens up a popup menu with all tags defined in the wiki starting with +"ind". + +Tags are also treated as |vimwiki-anchors| (similar to bold text). + +Note that tag search/jump/completion commands need certain metadata saved in +the wiki folder. This metadata can be manually updated by running +|:VimwikiRebuildTags|. There is an option |vimwiki-option-auto_tags|, when +enabled, tags metadata will be auto-updated on each page save. + +Once tags metadata has been built, one can use Vim's built-in tag search +functionality (see |tagsrch.txt|). + +Tags-related commands and options: + * |:VimwikiRebuildTags| + * |:VimwikiGenerateTags| + * |:VimwikiSearchTags| + * |vimwiki-option-auto_tags| ============================================================================== @@ -1581,12 +1636,13 @@ See |g:vimwiki_use_calendar| option to turn it off/on. ============================================================================== 12. Anchors *vimwiki-anchors* -Every header and every bold text is an anchor. To jump to it, use a wikilink +Every header, tag, and bold text is an anchor. To jump to it, use a wikilink of the form > [[file#anchor]] For example, consider the following file "Todo.wiki": > = My tasks = + :todo-lists: == Home == - [ ] bathe my dog == Work == @@ -1602,6 +1658,9 @@ Then, to jump from your index.wiki directly to your knitting projects, use: > Or, to jump to an individual project, use this link: > [[Todo#pig]] +Or, to jump to a tag, use this link: > + [[Todo#todo-lists]] + If there are multiple instances of an anchor, you can use the long form which consists of the complete header hierarchy, separated by '#': > [[Todo#My tasks#Knitting club#Knitting projects#dog]] @@ -2048,6 +2107,18 @@ local mappings |vimwiki_glstar|, |vimwiki_gl#| |vimwiki_gl-|, |vimwiki_gl-|, Note: if you use MediaWiki syntax, you probably would like to set this option to 0, because every indented line is considered verbatim text. + +*vimwiki-option-auto_tags* +------------------------------------------------------------------------------ +Key Default value Values~ +auto_tags 0 0, 1 + +Description~ +Set this option to 1 to automatically update the tags metadata when the +current wiki page is saved: > + let g:vimwiki_list = [{'path': '~/my_site/', 'auto_tags': 1}] + + ------------------------------------------------------------------------------ 12.4 Global Options *vimwiki-global-options* @@ -2646,6 +2717,7 @@ Vim plugins: http://www.vim.org/scripts/script.php?script_id=2226 ???~ + * Support for tags. * Support for wiki links absolute to the wiki root * The "file:" and "local:" schemes semantic changed slightly * Added the |VimwikiLinkConverter| function diff --git a/ftplugin/vimwiki.vim b/ftplugin/vimwiki.vim index 81cec3a..f34300c 100644 --- a/ftplugin/vimwiki.vim +++ b/ftplugin/vimwiki.vim @@ -31,26 +31,49 @@ execute 'setlocal suffixesadd='.VimwikiGet('ext') setlocal isfname-=[,] " gf}}} +exe "setlocal tags+=" . vimwiki#tags#metadata_file_path() + " MISC }}} " COMPLETION {{{ function! Complete_wikifiles(findstart, base) if a:findstart == 1 - let column = col('.')-1 + let column = col('.')-2 let line = getline('.')[:column] - let startoflink = match(line, '\[\[\zs[^\\[]*$') + let startoflink = match(line, '\[\[\zs[^\\[\]]*$') if startoflink != -1 + let s:line_context = '[' return startoflink endif if VimwikiGet('syntax') ==? 'markdown' - let startofinlinelink = match(line, '\[.*\](\zs.*$') + let startofinlinelink = match(line, '\[.*\](\zs[^)]*$') if startofinlinelink != -1 + let s:line_context = '[' return startofinlinelink endif endif + let startoftag = match(line, ':\zs[^:[:space:]]*$') + if startoftag != -1 + let s:line_context = ':' + return startoftag + endif + let s:line_context = '' return -1 else - if a:base !~# '#' + " Completion works for wikilinks/anchors, and for tags. s:line_content + " tells us, which string came before a:base. There seems to be no easier + " solution, because calling col('.') here returns garbage. + if s:line_context == '' + return [] + elseif s:line_context == ':' + " Tags completion + let tags = vimwiki#tags#get_tags() + if a:base != '' + call filter(tags, + \ "v:val[:" . (len(a:base)-1) . "] == '" . substitute(a:base, "'", "''", '') . "'" ) + endif + return tags + elseif a:base !~# '#' " we look for wiki files if a:base =~# '^wiki\d:' @@ -290,6 +313,14 @@ command! -buffer VimwikiTableMoveColumnRight call vimwiki#tbl#move_column_right( command! -buffer VimwikiDiaryNextDay call vimwiki#diary#goto_next_day() command! -buffer VimwikiDiaryPrevDay call vimwiki#diary#goto_prev_day() +" tags commands +command! -buffer -bang + \ VimwikiRebuildTags call vimwiki#tags#update_tags(1, '') +command! -buffer -nargs=* -complete=custom,vimwiki#tags#complete_tags + \ VimwikiSearchTags VimwikiSearch /::/ +command! -buffer -nargs=* -complete=custom,vimwiki#tags#complete_tags + \ VimwikiGenerateTags call vimwiki#tags#generate_tags() + " COMMANDS }}} " KEYBINDINGS {{{ @@ -622,6 +653,13 @@ if VimwikiGet('auto_toc') au BufWritePre call vimwiki#base#table_of_contents(0) augroup END endif + +if VimwikiGet('auto_tags') + " Automatically update tags metadata on page write. + augroup vimwiki + au BufWritePost call vimwiki#tags#update_tags(0, '') + augroup END +endif " AUTOCOMMANDS }}} " PASTE, CAT URL {{{ diff --git a/plugin/vimwiki.vim b/plugin/vimwiki.vim index 584f026..b969ff8 100644 --- a/plugin/vimwiki.vim +++ b/plugin/vimwiki.vim @@ -414,6 +414,8 @@ let s:vimwiki_defaults.diary_link_fmt = '%Y-%m-%d' let s:vimwiki_defaults.custom_wiki2html = '' " let s:vimwiki_defaults.list_margin = -1 + +let s:vimwiki_defaults.auto_tags = 0 "}}} " DEFAULT options {{{ diff --git a/syntax/omnipresent_syntax.vim b/syntax/omnipresent_syntax.vim index 612e63d..aaf3545 100644 --- a/syntax/omnipresent_syntax.vim +++ b/syntax/omnipresent_syntax.vim @@ -13,12 +13,16 @@ let g:vimwiki_default_header_match = '^\s*\(=\{1,6}\)=\@!\s*__Header__\s*\1=\@!\ let g:vimwiki_default_bold_search = '\%(^\|\s\|[[:punct:]]\)\@<=\*\zs\%([^*`[:space:]][^*`]*[^*`[:space:]]\|[^*`[:space:]]\)\ze\*\%([[:punct:]]\|\s\|$\)\@=' let g:vimwiki_default_bold_match = '\%(^\|\s\|[[:punct:]]\)\@<=\*__Text__\*\%([[:punct:]]\|\s\|$\)\@=' let g:vimwiki_default_wikilink = '\[\[\zs[^\\\]|]\+\ze\%(|[^\\\]]\+\)\?\]\]' +let g:vimwiki_default_tag_search = '\(^\|\s\)\zs:\([^:''[:space:]]\+:\)\+\ze\(\s\|$\)' +let g:vimwiki_default_tag_match = '\(^\|\s\):\([^:''[:space:]]\+:\)*__Tag__:\([^:[:space:]]\+:\)*\(\s\|$\)' let g:vimwiki_markdown_header_search = '^\s*\(#\{1,6}\)\([^#].*\)$' let g:vimwiki_markdown_header_match = '^\s*\(#\{1,6}\)#\@!\s*__Header__\s*$' let g:vimwiki_markdown_bold_search = '\%(^\|\s\|[[:punct:]]\)\@<=\*\zs\%([^*`[:space:]][^*`]*[^*`[:space:]]\|[^*`[:space:]]\)\ze\*\%([[:punct:]]\|\s\|$\)\@=' let g:vimwiki_markdown_bold_match = '\%(^\|\s\|[[:punct:]]\)\@<=\*__Text__\*\%([[:punct:]]\|\s\|$\)\@=' let g:vimwiki_markdown_wikilink = g:vimwiki_default_wikilink "XXX plus markdown-style links +let g:vimwiki_markdown_tag_search = g:vimwiki_default_tag_search +let g:vimwiki_markdown_tag_match = g:vimwiki_default_tag_match let g:vimwiki_media_header_search = '^\s*\(=\{1,6}\)\([^=].*[^=]\)\1\s*$' let g:vimwiki_media_header_match = '^\s*\(=\{1,6}\)=\@!\s*__Header__\s*\1=\@!\s*$' @@ -27,3 +31,5 @@ let g:vimwiki_media_bold_match = '''''''__Text__''''''' " ^- this strange looking thing is equivalent to "'''__Text__'''" but since we later " want to call escape() on this string, we must keep it in single quotes let g:vimwiki_media_wikilink = g:vimwiki_default_wikilink +let g:vimwiki_media_tag_search = g:vimwiki_default_tag_search " XXX rework to mediawiki categories format? +let g:vimwiki_media_tag_match = g:vimwiki_default_tag_match " XXX rework to mediawiki categories format? diff --git a/syntax/vimwiki.vim b/syntax/vimwiki.vim index 4f7642d..ded43f3 100644 --- a/syntax/vimwiki.vim +++ b/syntax/vimwiki.vim @@ -489,6 +489,10 @@ if g:vimwiki_valid_html_tags != '' execute 'syntax match VimwikiComment /'.g:vimwiki_rxComment.'/ contains=@Spell' endif + +" tags +execute 'syntax match VimwikiTag /'.g:vimwiki_rxTags.'/' + " }}} " header groups highlighting "{{{ @@ -554,6 +558,7 @@ hi def link VimwikiListTodo VimwikiList hi def link VimwikiCheckBoxDone Comment hi def link VimwikiEmoticons Character hi def link VimwikiHR Identifier +hi def link VimwikiTag Keyword hi def link VimwikiDelText Constant hi def link VimwikiDelTextT VimwikiDelText diff --git a/syntax/vimwiki_default.vim b/syntax/vimwiki_default.vim index 6023539..6150f99 100644 --- a/syntax/vimwiki_default.vim +++ b/syntax/vimwiki_default.vim @@ -91,3 +91,5 @@ let g:vimwiki_rxMathStart = '{{\$' let g:vimwiki_rxMathEnd = '}}\$' let g:vimwiki_rxComment = '^\s*%%.*$' +let g:vimwiki_rxTags = '\%(^\|\s\)\@<=:\%([^:''[:space:]]\+:\)\+\%(\s\|$\)\@=' +" see also g:vimwiki_default_tag_search diff --git a/syntax/vimwiki_markdown.vim b/syntax/vimwiki_markdown.vim index 31667e2..c32c7d7 100644 --- a/syntax/vimwiki_markdown.vim +++ b/syntax/vimwiki_markdown.vim @@ -88,3 +88,4 @@ let g:vimwiki_rxMathStart = '\$\$' let g:vimwiki_rxMathEnd = '\$\$' let g:vimwiki_rxComment = '^\s*%%.*$' +let g:vimwiki_rxTags = '\%(^\|\s\)\@<=:\%([^:[:space:]]\+:\)\+\%(\s\|$\)\@=' diff --git a/syntax/vimwiki_media.vim b/syntax/vimwiki_media.vim index 37c24db..9c0498e 100644 --- a/syntax/vimwiki_media.vim +++ b/syntax/vimwiki_media.vim @@ -69,3 +69,4 @@ let g:vimwiki_rxMathStart = '{{\$' let g:vimwiki_rxMathEnd = '}}\$' let g:vimwiki_rxComment = '^\s*%%.*$' +let g:vimwiki_rxTags = '\%(^\|\s\)\@<=:\%([^:[:space:]]\+:\)\+\%(\s\|$\)\@='