" Vim script that turns Vim into a personal wiki " Maintainer: Daniel Choi " License: MIT License (c) 2011 Daniel Choi if exists("g:SoyWikiLoaded") || &cp || version < 700 finish endif let g:SoyWikiLoaded = 1 let mapleader = ',' " This regex matches namedspaced WikiWords and unqualified WikiWords let s:wiki_link_pattern = '\C\m\<\([a-z][[:alnum:]_]\+\.\)\?[A-Z][a-z]\+[A-Z]\w*\>' let s:http_link_pattern = 'https\?:[^ >)\]]\+' let s:wiki_or_web_link_pattern = '\C\<\([a-z][[:alnum:]_]\+\.\)\?[A-Z][a-z]\+[A-Z]\w*\>\|https\?:[^ >)\]]\+' let s:rename_links_command = 'soywiki-rename ' let s:find_pages_linking_in_command = 'soywiki-pages-linking-in ' let s:expand_command = 'soywiki-expand ' let s:ls_command = 'soywiki-ls-t ' let s:search_for_link = "" func! s:trimString(string) let string = substitute(a:string, '\s\+$', '', '') return substitute(string, '^\s\+', '', '') endfunc func! s:page_title() return substitute(bufname(''), '\/', '.', '') endfunc func! s:display_missing_namespace_error(num_segments, page_title) if a:num_segments == 1 call s:error("Invalid wiki page: ".a:page_title." is missing a namespace. Put it in a namespace subdirectory.") elseif a:num_segments > 2 call s:error("Invalid wiki page: ".a:page_title." is nested too deeply. Namespaces are limited to one level.") endif endfunc func! s:display_invalid_wiki_word_error(word) call s:error(a:word . " is not a valid WikiWord.") endfunc func! s:namespace_of_title(page_title) let segments = split(a:page_title, '[./]') " page must have namespace if len(segments) == 2 return get(segments, 0) else call s:display_missing_namespace_error(len(segments), a:page_title) return "" endif endfunc func! s:page_namespace() return s:namespace_of_title(s:page_title()) endfunc func! s:title_without_namespace(page_title) let segments = split(a:page_title, '\.') if len(segments) == 2 return "." . get(segments, 1) else call s:display_missing_namespace_error(len(segments), a:page_title) endif endfunc " returns 1 or 0 func! s:has_namespace(link) return (match(a:link, '\w\.') != -1) endfunc " adds current page's namespace to the link func! s:infer_namespace(link) if s:has_namespace(s:filename2pagetitle(a:link)) return s:filename2pagetitle(a:link) else let x = s:page_namespace() . "." . a:link return x endif endfunc func! s:valid_wiki_word(link) return (match(a:link, s:wiki_link_pattern) == 0) endfunc func! s:is_wiki_page() return s:valid_wiki_word(s:page_title()) endfunc func! s:pagetitle2file(page) return substitute(a:page, '\.', '/', 'g') endfunc func! s:filename2pagetitle(page) return substitute(a:page, '/', '.', 'g') endfunc func! s:list_pages() let s:search_for_link = "" let pages = s:get_page_list() if len(pages) == 0 echom "There are no wiki pages yet but this one." else call s:page_list_window(pages, 'select-page', "Select page: ") end endfunc func! s:list_namespaces() let s:search_for_link = "" let pages = s:get_namespace_list() call s:page_list_window(pages, 'select-page', "Select namespace: ") endfunc func! s:trim_link(link) let link = matchstr(a:link, s:wiki_link_pattern) return link endfunc " returns a fully namespaced link func! s:link_under_cursor() let link = s:trim_link(expand("")) " strip off non-letters at the end and beginning (e.g., a comma) if ! s:has_namespace(link) let link = s:infer_namespace(link) endif if match(link, s:wiki_link_pattern) == -1 return "" else return link end endfunc func! s:find_next_wiki_link(backward) let n = 0 " don't wrap if a:backward == 1 normal lb let result = search(s:wiki_or_web_link_pattern, 'Wb') else let result = search(s:wiki_or_web_link_pattern, 'W') endif if (result == 0) return "" end return s:link_under_cursor() endfunc func! s:follow_link_under_cursor(split) let word = expand("") if match(word, s:http_link_pattern) != -1 call s:open_href_under_cursor() return endif let link = s:link_under_cursor() if link == "" echom "" return "" elseif line('.') == 1 " SPECIAL CASE " close window if winnr('$') > 1 close endif return else call s:load_page(link, a:split) endif endfunc " If no link under cursor, tries to find the next one func! s:fuzzy_follow_link(split) let link = s:link_under_cursor() if link == "" let link = s:find_next_wiki_link(0) if link == "" echom "No links found" return endif endif call s:load_page(link, a:split) endfunc " -------------------------------------------------------------------------------- " LOAD PAGE func! s:load_page(page, split) if (s:is_wiki_page()) write endif let file = s:pagetitle2file(a:page) let title = s:filename2pagetitle(a:page) if bufwinnr(file) != -1 exec bufwinnr(file)."wincmd w" return endif if (!filereadable(file)) " create the file let namespace = s:namespace_of_title(a:page) if namespace == "" return end call system("mkdir -p " . namespace) call writefile([title, '', ''], file) endif if (a:split == 2) exec "botright vsplit ". file elseif (a:split == 1) exec "rightbelow split ". file elseif (a:split == 0) exec "e ".file endif if s:search_for_link != '' let res = search(s:search_for_link, 'cw') let s:search_for_link = '' endif endfunc func! s:load_most_recently_modified_page(index) let pages = split(system(s:ls_command), "\n") let start_page = len(pages) > a:index ? get(pages, a:index) : "main.HomePage" call s:load_page(start_page, 0) endfunc func! s:delete_page() let file = bufname('%') let bufnr = bufnr('%') if winnr('$') == 1 " we need a buffer to replace this one with " go to previous buffer let next_window = bufnr('#') if next_window == -1 " TODO this fails to execute call s:load_page("main.HomePage", 0) else exec ":b".next_window endif wincmd p endif echo system("git rm " . file) echo system("rm " . file) call system("git commit -am 'page deletion'") exec "bdelete " . bufnr redraw echom "Deleted " . file endfunc func! s:rename_page(page_path_or_title) let page_title = s:infer_namespace(a:page_path_or_title) let newfile = s:pagetitle2file(page_title) if (filereadable(newfile)) exe "echom '" . newfile . " already exists!'" return endif if s:valid_wiki_word(page_title) write! call system("mkdir -p " . s:namespace_of_title(page_title)) let original_file = bufname('') if executable("git") echo system("git mv " . original_file . " " . newfile) else echo system("mv " . original_file . " " . newfile) endif call system("git commit -am 'rename wiki page'") exec "!" . s:rename_links_command . original_file . " " . newfile call system("git commit -am 'rename wiki links'") exec "e " . newfile else call s:display_invalid_wiki_word_error(page_title) endif endfunc func! s:create_page(page_path) let page_title = s:infer_namespace(a:page_path) let page_path = s:pagetitle2file(page_title) if (filereadable(page_path)) exe "echom '" . page_path . " already exists! Loaded.'" endif if s:valid_wiki_word(page_title) call s:load_page(s:filename2pagetitle(page_path), 0) else call s:display_invalid_wiki_word_error(page_title) endif endfunc func! s:save_revision() call system("git add " . bufname('%')) call system("git commit -a -m 'edit'") endfunc func! s:show_revision_history(stat) if (a:stat) exec ":!git log --stat " . bufname('%') else exec ":!git log -p " . bufname('%') end endfunc func! s:show_blame() exec ":! git blame --date=relative " . bufname('%') endfunc " ------------------------------------------------------------------------------- " select Page func! s:omit_this_page(page_list) if exists("s:return_to_bufname") let page_list = filter( a:page_list, 'v:val != "'.s:return_to_bufname.'"') return page_list else return a:page_list endif endfunc " func! s:get_page_list() " no file current in buffer if len(bufname('')) == 0 let pages = split(system(s:ls_command), "\n") elseif bufname('') == 'pages-linking-in' " this needs refactoring to rely less on state let pages = s:pages_linking_in else "let pages = s:omit_this_page(split(system(s:ls_command), "\n")) let pages = split(system(s:ls_command), "\n") endif return pages endfunction func! s:get_namespace_list() let pages = split(system(s:ls_command . " -n"), "\n") return pages endfunction func! s:pages_in_this_namespace(pages) let namespace = s:page_namespace() let pages = filter( a:pages, 'v:val =~ "^' . namespace . '\."') " strip leading namespace return map(pages, "substitute(v:val, '^" . namespace . "\.', '', '') ") endfunc " When user press TAB after typing a few characters in the page selection " window, if the user started typing a namespace (which starts with a " lowercase letter), try to complete it. Otherwise take no action. func! s:reduce_matches() if (!exists("s:matching_pages")) return endif let fragment = expand("") " find the first namespace in the list let namespaced_matches = filter( s:matching_pages, 'v:val =~ "^' . fragment . '\."') if (len(namespaced_matches) == 0) return elseif match(fragment, '^[a-z]') == 0 && match(fragment, '\.' == -1) " we're beginning to type a namespace let namespace = get(split(get(namespaced_matches, 0), '\.'), 0) let namespace .= "." call feedkeys( "BcW" . namespace . "\\\" , "t") else return endif endfunc function! s:page_list_window(page_match_list, buffer_name, prompt) " remember the original window let s:return_to_winnr = winnr() let s:return_to_bufname = s:filename2pagetitle(bufname('')) let s:matching_pages = a:page_match_list exec "leftabove split ".a:buffer_name setlocal completefunc=CompletePageTitle setlocal buftype=nofile setlocal noswapfile setlocal modifiable setlocal textwidth=0 resize 1 inoremap :call select_page() inoremap :call reduce_matches() noremap q :close inoremap :close " Bad, gets buggy with frag "dai" " autocmd CursorMovedI call feedkeys("\\\", "n") " c-p clears the line call setline(1, a:prompt) normal $ call feedkeys("a\\\", 't') " call feedkeys("a", 't') endfunction function! CompletePageTitle(findstart, base) if a:findstart " locate the start of the word let line = getline('.') let start = col('.') - 1 while start > 0 && line[start - 1] =~ '\m[[:alnum:]\.]' let start -= 1 endwhile return start else let base = s:trimString(a:base) if (base == '') return s:matching_pages else let res = [] if bufname('') == 'select-page' let pages = s:get_page_list() for m in pages if m =~ '\c' . base call add(res, m) endif endfor else " autocomplete inline if ! exists("s:matching_pages") let s:matching_pages = s:get_page_list() endif let pages = base =~ '\C^[a-z]' ? s:matching_pages[:] : s:pages_in_this_namespace(s:matching_pages[:]) for m in pages if m =~ '^\c' . base call add(res, m) endif endfor endif return res endif endif endfun function! s:select_page() let page = s:trimString( get(split(getline(line('.')), ": "), 1) ) close exe s:return_to_winnr . "wincmd w" if (page == '') " no selection return end " if time is just a namespace, append .HomePage to it if page =~ '^[a-z][[:alnum:]_]\+$' let page = page . ".HomePage" endif for item in s:matching_pages if (page =~ item) call s:load_page(page, 0) break end endfor endfunction "------------------------------------------------------------------------ " PAGES LINKING IN " " this logic could be more precise, in cases where pages have same name " in different namespaces func! s:list_pages_linking_in() let s:pages_linking_in = split(system(s:find_pages_linking_in_command . s:page_title()), "\n") " cursor should jump to this string after the selected page is loaded: let s:search_for_link = s:title_without_namespace(s:page_title()) if len(s:pages_linking_in) == 1 call s:load_page(get(s:pages_linking_in, 0), 0) elseif len(s:pages_linking_in) == 0 echom "No pages link to " . s:page_title() . "!" else call s:page_list_window(s:pages_linking_in, "pages-linking-in", "Pages that link to " . s:page_title() . ": ") endif endfunc "------------------------------------------------------------------------ " This appends the selected text (use visual-mode) to the page selected " in the page selection window. func! s:extract(...) range if a:0 != 3 return s:error("Incorrect number of arguments") endif let first = a:firstline let last = a:lastline let file = a:1 if match(file, s:wiki_link_pattern) == -1 echom "Target page must be a WikiWord!" return endif let mode = a:2 " append or insert let link = a:3 " replace with link ? let range = first.",".last silent exe range."yank" if link let replacement = s:filename2pagetitle(file) silent exe "norm! :".first.",".last."change\".replacement."\.\" else " this one just deletes the line silent exe "norm! :".first.",".last."change\.\" endif write! if bufnr(file) == -1 || bufwinnr(bufnr(file)) == -1 if !filereadable(file) " create the file let page_title = s:filename2pagetitle(file) let namespace = s:namespace_of_title(page_title) if len(namespace) > 0 call system("mkdir -p " . namespace) endif call writefile([page_title, '', ''], file) endif exec "botright vsplit ".file else let targetWindow = bufwinnr(bufnr(file)) exe targetWindow."wincmd w" end if mode == 'append' normal G silent put silent put= '' elseif mode == 'insert' call cursor(2, 0) silent put end write! endfunc func! s:error(str) echohl ErrorMsg echomsg a:str echohl None endfunction func! s:insert_divider() let divider = '------------------------------------------------------------------------' silent put! =divider silent put='' endfunc "------------------------------------------------------------------------ " SEARCH func! s:wiki_search(pattern, in_this_namespace) let pattern = (empty(a:pattern) ? @/ : a:pattern) if a:in_this_namespace execute printf('vimgrep/\c%s/ %s', pattern, s:page_namespace()."/*") else execute printf('vimgrep/\c%s/ %s', pattern, "*/*") endif endfunc "------------------------------------------------------------------------ " This opens a new buffer with all the lines with just WikiLinks on them " expanded (recursively). This is not a wiki buffer but a text buffer func! s:expand(seamless, vertical) if a:seamless == 1 " seamful, the default echom "Expanding seamfully. Please wait." let res = system(s:expand_command . " seamless " . bufname('%')) else " seamless echom "Expanding seamlessly. Please wait." let res = system(s:expand_command . " seamful " . bufname('%')) endif if a:vertical botright vnew else new endif setlocal buftype=nofile "scratch buffer for viewing; user can write silent! put =res silent! 1delete silent! normal 1G redraw echom "Expanded " . (a:seamless == 0 ? 'seamfully' : 'seamlessly') . "." endfunc "------------------------------------------------------------------------ func! s:open_href_under_cursor() let href = expand("") let command = g:SoyWiki#browser_command . " '" . href . "' " call system(command) echom command endfunc func! s:find_next_href_and_open() let res = search(s:http_link_pattern, 'cw') if res != 0 call s:open_href_under_cursor() endif endfunc func! s:goto_homepage(main) if a:main call s:load_page("main.HomePage", 0) else let namespace_home = s:page_namespace()."/HomePage" call s:load_page(namespace_home, 0) endif endfunc " -------------------------------------------------------------------------------- " HELP func! s:show_help() let command = g:SoyWiki#browser_command . ' ' . shellescape('http://danielchoi.com/software/soywiki.html') call system(command) endfunc "------------------------------------------------------------------------ func! s:global_mappings() nnoremap m :call list_pages() nnoremap M :call list_pages_linking_in() nnoremap n :call list_namespaces() nnoremap o :call find_next_href_and_open() nnoremap q :close " for netrw vertical split nnoremap ,O :exec "silent botright vsplit ". expand("") command! -bar -nargs=1 -range -complete=file SWAppend :,call s:extract(, 'append', 0) command! -bar -nargs=1 -range -complete=file SWInsert :,call s:extract(, 'insert', 0) command! -bar -nargs=1 -range -complete=file SWLinkAppend :,call s:extract(, 'append', 1) command! -bar -nargs=1 -range -complete=file SWLinkInsert :,call s:extract(, 'insert', 1) command! -bar -nargs=1 SWSearch :call s:wiki_search(, 0) command! -bar -nargs=1 SWNamespaceSearch :call s:wiki_search(, 1) autocmd BufReadPost,BufNewFile,WinEnter,BufEnter,BufNew,BufAdd * call s:highlight_wikiwords() autocmd BufReadPost,BufNewFile,WinEnter,BufEnter,BufNew,BufAdd * call s:prep_buffer() " autocmd BufEnter * call s:prep_buffer() endfunc " this checks if the buffer is a SoyWiki file (from firstline) " and then turns on syntax coloring and mappings as necessary func! s:prep_buffer() if (s:is_wiki_page() && !exists("b:mappings_loaded")) " let user decide on the textwidth set filetype=txt nnoremap :call follow_link_under_cursor(0) nnoremap :call follow_link_under_cursor(2) nnoremap :call follow_link_under_cursor(1) noremap f :call fuzzy_follow_link(0) noremap :call find_next_wiki_link(0) noremap :call find_next_wiki_link(1) command! -bar -nargs=1 -range -complete=file SWCreate :call create_page() command! -bar -nargs=1 -range -complete=file SWRenameTo :call rename_page() command! -buffer SWDelete :call s:delete_page() command! -buffer SWLog :call s:show_revision_history(0) noremap lp :call show_revision_history(0) command! -buffer SWLogStat :call s:show_revision_history(1) noremap ls :call show_revision_history(1) command! -buffer SWBlame :call s:show_blame() noremap b :call show_blame() noremap x :call expand(0,1) noremap X :call expand(1,1) noremap xx :call expand(0,0) noremap XX :call expand(1,0) noremap h :call goto_homepage(0) noremap H :call goto_homepage(1) noremap ? :call show_help() nnoremap \ gqap nnoremap - ok72i- nnoremap d :r !dateo nnoremap D :r !datek72i-jo " set nu setlocal completefunc=CompletePageTitle augroup save_revision au! autocmd FileWritePost,BufWritePost,BufUnload call s:save_revision() augroup END let b:mappings_loaded = 1 endif endfunc func! s:highlight_wikiwords() if (s:is_wiki_page()) "syntax clear exe "syn match Comment /". s:wiki_link_pattern. "/" exe "syn match Constant /". s:http_link_pattern . "/" endif endfunc call s:global_mappings() if (!isdirectory(".git")) call system("git init") echom "Created .git repository to store revisions" endif " compress the repo call system("git gc") if !exists("g:SoyWiki#browser_command") for cmd in ["gnome-open", "open"] if executable(cmd) let g:SoyWiki#browser_command = cmd break endif endfor if !exists("g:SoyWiki#browser_command") echom "Can't find the to open your web browser." endif endif if len(bufname("%")) == 0 call s:load_most_recently_modified_page(0) else call s:load_page(bufname("%"), 0) endif syntax enable call s:highlight_wikiwords() call s:prep_buffer()