From a5c99e3d54ed47874e16ecb57faa625e14e2a395 Mon Sep 17 00:00:00 2001 From: Daniel Ashbrook Date: Tue, 26 Oct 2021 10:47:29 +0200 Subject: [PATCH 1/3] Tag renaming --- autoload/wiki/buffer.vim | 5 +- autoload/wiki/tags.vim | 126 ++++++++++++++++++++++++++++ doc/wiki.txt | 27 +++++- test/test-tags/test-custom-yaml.vim | 8 +- test/test-tags/test-defaults.vim | 11 ++- test/wiki-basic/tagged.wiki | 2 +- 6 files changed, 170 insertions(+), 9 deletions(-) diff --git a/autoload/wiki/buffer.vim b/autoload/wiki/buffer.vim index dc5fdf33..d53112a8 100644 --- a/autoload/wiki/buffer.vim +++ b/autoload/wiki/buffer.vim @@ -33,8 +33,6 @@ function! wiki#buffer#init() abort " {{{1 endfunction " }}}1 - - function! s:init_buffer_commands() abort " {{{1 command! -buffer WikiGraphFindBacklinks call wiki#graph#find_backlinks() command! -buffer WikiGraphCheckLinks call wiki#graph#check_links() @@ -59,6 +57,7 @@ function! s:init_buffer_commands() abort " {{{1 command! -buffer WikiTagReload call wiki#tags#reload() command! -buffer -nargs=* WikiTagList call wiki#tags#list() command! -buffer -nargs=* WikiTagSearch call wiki#tags#search() + command! -buffer -nargs=+ -complete=custom,wiki#tags#get_tag_names WikiTagRename call wiki#tags#rename_ask() command! -buffer WikiFzfToc call wiki#fzf#toc() @@ -95,6 +94,7 @@ function! s:init_buffer_mappings() abort " {{{1 nnoremap (wiki-tag-list) :WikiTagList nnoremap (wiki-tag-reload) :WikiTagReload nnoremap (wiki-tag-search) :WikiTagSearch + nnoremap (wiki-tag-rename) :WikiTagRename nnoremap (wiki-fzf-toc) :WikiFzfToc inoremap (wiki-fzf-toc) :WikiFzfToc @@ -145,6 +145,7 @@ function! s:init_buffer_mappings() abort " {{{1 \ '(wiki-tag-list)': 'wsl', \ '(wiki-tag-reload)': 'wsr', \ '(wiki-tag-search)': 'wss', + \ '(wiki-tag-rename)': 'wsn', \ 'x_(wiki-link-toggle-visual)': '', \ 'o_(wiki-au)': 'au', \ 'x_(wiki-au)': 'au', diff --git a/autoload/wiki/tags.vim b/autoload/wiki/tags.vim index f1357f4b..66a6c827 100644 --- a/autoload/wiki/tags.vim +++ b/autoload/wiki/tags.vim @@ -8,6 +8,11 @@ function! wiki#tags#get_all() abort " {{{1 return s:tags.gather() endfunction +" }}}1 +function! wiki#tags#get_tag_names(...) abort " {{{1 + return join(map(keys(wiki#tags#get_all()), 'escape(v:val, " ")'), "\n") +endfunction + " }}}1 function! wiki#tags#search(...) abort " {{{1 let l:cfg = deepcopy(g:wiki_tag_search) @@ -71,8 +76,40 @@ function! wiki#tags#reload() abort " {{{1 endfunction " }}}1 +function! wiki#tags#rename(old_tag, new_tag='', rename_to_existing=0) abort " {{{1 + if a:new_tag ==# '' + call wiki#tags#rename_ask(a:old_tag) + else + call s:tags.rename(a:old_tag, a:new_tag, a:rename_to_existing) + endif +endfunction + +" }}}1 +function! wiki#tags#rename_ask(old_tag='', new_tag='', ...) abort " {{{1 + let l:old_tag = a:old_tag ==# '' ? + \ input('Enter tag to rename (wihtout delimiters): ', '', 'custom,wiki#tags#get_tag_names') : + \ a:old_tag + if l:old_tag ==# '' | return | endif + + if input('Rename "' . l:old_tag . '"' + \ . (a:new_tag ==# '' ? '' : ' to "' . a:new_tag . '"') + \ . ' [y]es/[N]o? ') !~? '^y' + return + endif + + " Get new tag name + redraw! + if a:new_tag ==# '' + call wiki#log#info('Enter new tag name (without tag delimiters):') + let l:new_tag = input('> ') + else + let l:new_tag = a:new_tag + endif + call wiki#tags#rename(l:old_tag, l:new_tag) +endfunction +" }}}1 function! s:search(cfg) abort " {{{1 call s:tags.gather() let l:tags = get(s:tags.collection, a:cfg.tag, []) @@ -331,7 +368,92 @@ function! s:tags.add(tag, ...) abort dict " {{{1 endfunction " }}}1 +function! s:tags.rename(old_tag, new_tag, rename_to_existing=0) abort dict " {{{1 + redraw! + if !has_key(self.collection, a:old_tag) + call wiki#log#info('Old tag name "' . a:old_tag . '" not found in cache; reloading tags.') + call wiki#tags#reload() + if !has_key(self.collection, a:old_tag) + return wiki#log#warn('No tag named "' . a:old_tag . '", aborting rename.') + endif + endif + + if has_key(self.collection, a:new_tag) + call wiki#log#warn('Tag "' . a:new_tag . '" already exists!') + if !a:rename_to_existing + if input('Rename anyway? [y]es/[N]o: ', 'N') ==? 'n' + return + endif + endif + endif + + call wiki#log#info('Renaming tag "' . a:old_tag . '" to "' . a:new_tag . '".') + + let l:tagpages = self.collection[a:old_tag] + + let l:bufnr = bufnr('') + " Get list of open wiki buffers + let l:bufs = + \ map( + \ filter( + \ filter(range(1, bufnr('$')), 'buflisted(v:val)'), + \ '!empty(getbufvar(v:val, ''wiki''))'), + \ 'fnamemodify(bufname(v:val), '':p'')') + + " Save other wiki buffers + for l:bufname in l:bufs + execute 'buffer' fnameescape(l:bufname) + update + endfor + + let l:num_files = 0 + + " We already know where the tag is in the file, thanks to the cache + for [l:file, l:lnum] in l:tagpages + if s:update_tag_in_wiki(l:file, l:lnum, a:old_tag, a:new_tag) + call self.add(a:new_tag, l:file, l:lnum) + call remove(self.collection, a:old_tag) + let l:num_files += 1 + endif + endfor + + " Refresh other wiki buffers + for l:bufname in l:bufs + execute 'buffer' fnameescape(l:bufname) + edit + endfor + + " Refresh tags + silent call wiki#tags#reload() + + execute 'buffer' l:bufnr + + call wiki#log#info(printf('Renamed tags in %d files', l:num_files)) + +endfunction + +" }}}1 +function! s:update_tag_in_wiki(path, lnum, old_tag, new_tag) abort + call wiki#log#info('Renaming tag in: ' . fnamemodify(a:path, ':t')) + let l:lines = readfile(a:path) + let l:tagline = l:lines[a:lnum-1] + for l:parser in g:wiki_tag_parsers + if l:parser.match(l:tagline) + let l:tags = l:parser.parse(l:tagline) + if index(l:tags, a:new_tag) >= 0 + call filter(l:tags, {_, t -> t !=# a:old_tag}) + else + call map(l:tags, {_, t -> t ==# a:old_tag ? a:new_tag : t}) + endif + let l:lines[a:lnum-1] = l:parser.make(l:tags, l:tagline) + call writefile(l:lines, a:path) + return 1 + endif + endfor + return wiki#log#error("Didn't match tagline " . a:path . ':' . a:lnum . ' with any parser') +endfunction +" }}}1 function! s:parse_tags_in_file(file) abort " {{{1 let l:tags = [] let l:lnum = 0 @@ -390,4 +512,8 @@ function! g:wiki#tags#default_parser.parse(line) dict abort return l:tags endfunction +function! g:wiki#tags#default_parser.make(taglist, curline='') dict abort + return empty(a:taglist) ? '' : join(map(a:taglist, '":" . v:val . ":"')) +endfunction + " }}}1 diff --git a/doc/wiki.txt b/doc/wiki.txt index 555b9de9..9ac59565 100644 --- a/doc/wiki.txt +++ b/doc/wiki.txt @@ -682,6 +682,12 @@ OPTIONS *wiki-config-options* A function (|FuncRef|) that takes a single argument, the current line. It should return a list of the tags in the given line. + make~ + A function (|FuncRef|) that takes two arguments: a list of tags, and the + current line. It should return a new line with the list of tags + formatted appropriately. This new line will replace the current line. + This function is used by |WikiTagRename|. + re_findstart~ `OPTIONAL` A regular expression (|String|) that should match the start of the @@ -694,7 +700,8 @@ OPTIONS *wiki-config-options* let g:wiki_tag_parsers = [ \ g:wiki#tags#default_parser, \ { 'match': {x -> x =~# '^tags: '}, - \ 'parse': {x -> split(matchstr(x, '^tags:\zs.*'), '[ ,]\+')}} + \ 'parse': {x -> split(matchstr(x, '^tags:\zs.*'), '[ ,]\+')}, + \ 'make': {t,x -> 'tags: ' . empty(t) ? '' : join(t, ', ')}} \] < See |g:wiki#tags#default_parser| for details on the default tag parser. @@ -1061,6 +1068,12 @@ the commands are also available as mappings of the form `(wiki-[name])`. * `scratch` Add results to a scratch buffer * `cursor` Insert result below cursor +*(wiki-tag-rename)* +*WikiTagRename* [old_tag] [new_tag] + Rename `old_tag` to `new_tag` in all pages. If zero or one arguments are + supplied, the command asks for input. This command uses the `make` function + of |g:wiki_tag_parsers|. + *CtrlPWiki* Open |CtrlP| in find file mode for wiki files in current wiki or in the main wiki defined by |g:wiki_root|. @@ -1423,6 +1436,7 @@ Related commands: - |WikiTagList| - |WikiTagReload| - |WikiTagSearch| +- |WikiTagRename| - |WikiFzfTags| Related settings: @@ -1462,13 +1476,18 @@ for convenience: > return l:tags endfunction -One may customize the default parser by adjusting the `re_match` and -`re_findstart` strings. E.g., to instead recognize hashed tags like `#tag1` -and `#my-other-tag`, one could add the following in ones vimrc: > + function! g:wiki#tags#default_parser.make(taglist, curline='') dict abort + return empty(a:taglist) ? '' : join(map(a:taglist, '":" . v:val . ":"')) + endfunction + +One may customize the default parser by adjusting `re_match`, +`re_findstart`, and `make`. E.g., to instead recognize hashed tags like `#tag1` +and `#my-other-tag`, one could add the following in one's vimrc: > let s:tag_parser = deepcopy(g:wiki#tags#default_parser) let s:tag_parser.re_match = '\v%(^|\s)#\zs[^# ]+' let s:tag_parser.re_findstart = '\v%(^|\s)#\zs[^# ]+' + let s:tag_parser.make = {t,l -> empty(t) ? '' : join(map(t, '"#" . v:val))} let g:wiki_tag_parsers = [s:tag_parser] diff --git a/test/test-tags/test-custom-yaml.vim b/test/test-tags/test-custom-yaml.vim index 353c479d..423f5084 100644 --- a/test/test-tags/test-custom-yaml.vim +++ b/test/test-tags/test-custom-yaml.vim @@ -8,7 +8,8 @@ let g:wiki_link_extension = '.md' let g:wiki_tag_parsers = [ \ g:wiki#tags#default_parser, \ { 'match': {x -> x =~# '^tags: '}, - \ 'parse': {x -> split(matchstr(x, '^tags:\zs.*'), '[ ,]\+')}} + \ 'parse': {x -> split(matchstr(x, '^tags:\zs.*'), '[ ,]\+')}, + \ 'make': {t,l -> empty(t) ? '' : 'tags: ' . join(t, ', ')}} \] silent edit ../wiki-markdown/index.md @@ -16,4 +17,9 @@ silent edit ../wiki-markdown/index.md let s:tags = wiki#tags#get_all() call assert_equal(['drink', 'good', 'life', 'work'], sort(keys(s:tags))) +call wiki#tags#rename('drink', 'coffee', 1) +call wiki#tags#rename('work', 'coffee', 1) +call wiki#tags#rename('life', 'good', 1) +call assert_equal('tags: coffee, good', readfile('../wiki-markdown/yaml-tags.md')[2]) + call wiki#test#finished() diff --git a/test/test-tags/test-defaults.vim b/test/test-tags/test-defaults.vim index e870ac4a..04e025fd 100644 --- a/test/test-tags/test-defaults.vim +++ b/test/test-tags/test-defaults.vim @@ -6,8 +6,17 @@ let g:wiki_cache_persistent = 0 silent edit ../wiki-basic/index.wiki let s:tags = wiki#tags#get_all() -call assert_equal(5, len(s:tags)) +call assert_equal(8, len(s:tags)) call assert_equal(1, len(s:tags.tag1)) call assert_equal(2, len(s:tags.marked)) +"Original line in tagged.wiki is: +":tag_six: :tag3: :tag4: :tag5: :tag6: +call wiki#tags#rename('tag4', 'tag_four', 1) "Basic renaming +call wiki#tags#rename('tag5', 'tag_four', 1) "Rename to an existing tag +call wiki#tags#rename('tag6', 'tag_six', 1) "Rename to an existing tag, keeping original ordering +call assert_equal( + \ ':tag_six: :tag3: :tag_four:', + \ readfile('../wiki-basic/tagged.wiki')[1]) + call wiki#test#finished() diff --git a/test/wiki-basic/tagged.wiki b/test/wiki-basic/tagged.wiki index 6b4b8abf..b8b0805a 100644 --- a/test/wiki-basic/tagged.wiki +++ b/test/wiki-basic/tagged.wiki @@ -1,5 +1,5 @@ # Intro -:tag3: :tag4: +:tag_six: :tag3: :tag4: :tag5: :tag6: This is a wiki. From 1241f01b87cbb8ab3804d770f3f3d48dd0cb66ad Mon Sep 17 00:00:00 2001 From: Daniel Ashbrook Date: Fri, 29 Oct 2021 20:12:54 +0200 Subject: [PATCH 2/3] Address spacing, line breaks, docs, function splits --- README.md | 3 +++ autoload/wiki/buffer.vim | 5 ++++- autoload/wiki/tags.vim | 25 ++++++++++++++++++------- doc/wiki.txt | 4 +++- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ee032c7c..381a2e83 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Or use some other plugin manager: * [neobundle](https://github.com/Shougo/neobundle.vim) * [pathogen](https://github.com/tpope/vim-pathogen) +Note that some features require a recent version of Vim (>= 8.1) or +NeoVim (>= 0.5). + ## Usage This outlines the basic steps to get started: diff --git a/autoload/wiki/buffer.vim b/autoload/wiki/buffer.vim index d53112a8..60ce84e8 100644 --- a/autoload/wiki/buffer.vim +++ b/autoload/wiki/buffer.vim @@ -33,6 +33,8 @@ function! wiki#buffer#init() abort " {{{1 endfunction " }}}1 + + function! s:init_buffer_commands() abort " {{{1 command! -buffer WikiGraphFindBacklinks call wiki#graph#find_backlinks() command! -buffer WikiGraphCheckLinks call wiki#graph#check_links() @@ -57,7 +59,8 @@ function! s:init_buffer_commands() abort " {{{1 command! -buffer WikiTagReload call wiki#tags#reload() command! -buffer -nargs=* WikiTagList call wiki#tags#list() command! -buffer -nargs=* WikiTagSearch call wiki#tags#search() - command! -buffer -nargs=+ -complete=custom,wiki#tags#get_tag_names WikiTagRename call wiki#tags#rename_ask() + command! -buffer -nargs=+ -complete=custom,wiki#tags#get_tag_names + \ WikiTagRename call wiki#tags#rename_ask() command! -buffer WikiFzfToc call wiki#fzf#toc() diff --git a/autoload/wiki/tags.vim b/autoload/wiki/tags.vim index 66a6c827..83f3dbd1 100644 --- a/autoload/wiki/tags.vim +++ b/autoload/wiki/tags.vim @@ -9,8 +9,17 @@ function! wiki#tags#get_all() abort " {{{1 endfunction " }}}1 -function! wiki#tags#get_tag_names(...) abort " {{{1 - return join(map(keys(wiki#tags#get_all()), 'escape(v:val, " ")'), "\n") +function! wiki#tags#get_tag_names() abort " {{{1 + return keys(wiki#tags#get_all()) +endfunction + +" }}}1 +function! wiki#tags#complete_tag_names(...) abort " {{{1 + " Returns tag names in newline separated string suitable for completion with + " the "custom" argument, see ":help :command-completion-custom". We could + " also have used "customlist", but with "custom", filtering is performed + " implicitly and may be more efficient (cf. documentation). + return join(map(wiki#tags#get_tag_names(), 'escape(v:val, " ")'), "\n") endfunction " }}}1 @@ -86,9 +95,10 @@ endfunction " }}}1 function! wiki#tags#rename_ask(old_tag='', new_tag='', ...) abort " {{{1 - let l:old_tag = a:old_tag ==# '' ? - \ input('Enter tag to rename (wihtout delimiters): ', '', 'custom,wiki#tags#get_tag_names') : - \ a:old_tag + let l:old_tag = a:old_tag ==# '' + \ ? input('Enter tag to rename (wihtout delimiters): ', + '', 'custom,wiki#tags#get_tag_names') + \ : a:old_tag if l:old_tag ==# '' | return | endif if input('Rename "' . l:old_tag . '"' @@ -110,6 +120,8 @@ function! wiki#tags#rename_ask(old_tag='', new_tag='', ...) abort " {{{1 endfunction " }}}1 + + function! s:search(cfg) abort " {{{1 call s:tags.gather() let l:tags = get(s:tags.collection, a:cfg.tag, []) @@ -368,7 +380,7 @@ function! s:tags.add(tag, ...) abort dict " {{{1 endfunction " }}}1 -function! s:tags.rename(old_tag, new_tag, rename_to_existing=0) abort dict " {{{1 +function! s:tags.rename(old_tag, new_tag, rename_to_existing=v:false) abort dict " {{{1 redraw! if !has_key(self.collection, a:old_tag) call wiki#log#info('Old tag name "' . a:old_tag . '" not found in cache; reloading tags.') @@ -429,7 +441,6 @@ function! s:tags.rename(old_tag, new_tag, rename_to_existing=0) abort dict " {{{ execute 'buffer' l:bufnr call wiki#log#info(printf('Renamed tags in %d files', l:num_files)) - endfunction " }}}1 diff --git a/doc/wiki.txt b/doc/wiki.txt index 9ac59565..a59bdb06 100644 --- a/doc/wiki.txt +++ b/doc/wiki.txt @@ -90,10 +90,12 @@ Note: |wiki.vim| is `not` a filetype plugin. It is designed so that it may be us REQUIREMENTS *wiki-intro-requirements* This plugin is mainly developed on and for Linux. Some or most of the features -should still work on Windows and OSX, but currently there are no guaranties. +should still work on Windows and OSX, but currently there are no guarantees. On Windows, it is assumed that users enable 'shellslash' to avoid issues with backslashes versus forward slashes. +Some features require a recent version of Vim (>= 8.1) or NeoVim (>= 0.5). + The following is a list of external tools that are used for some of the features: From e584c6d491493d49fb2206fef958139c5835b672 Mon Sep 17 00:00:00 2001 From: Daniel Ashbrook Date: Fri, 29 Oct 2021 21:12:24 +0200 Subject: [PATCH 3/3] Missing v:false --- autoload/wiki/tags.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/wiki/tags.vim b/autoload/wiki/tags.vim index 83f3dbd1..184f7043 100644 --- a/autoload/wiki/tags.vim +++ b/autoload/wiki/tags.vim @@ -85,7 +85,7 @@ function! wiki#tags#reload() abort " {{{1 endfunction " }}}1 -function! wiki#tags#rename(old_tag, new_tag='', rename_to_existing=0) abort " {{{1 +function! wiki#tags#rename(old_tag, new_tag='', rename_to_existing=v:false) abort " {{{1 if a:new_tag ==# '' call wiki#tags#rename_ask(a:old_tag) else