1 " Copyright (c) 2020 Dave Gauer
     
2 " MIT License
     
3 
     4 if exists('g:loaded_vviki')
     
5 	finish
     
6 endif
     
7 let g:loaded_vviki = 1
     
8 
     9 " Initialize configuration defaults
    
10 " See 'Configuration' in the help documentation for full explanations.
    
11 "
    
12 if !exists('g:vviki_root')
    
13     " Default root directory for (current) wiki
    
14 	let g:vviki_root = "~/wiki"
    
15 endif
    
16 
    17 if !exists('g:vviki_ext')
    
18     " Extension to append to pages when navigating internal links
    
19 	let g:vviki_ext = ".adoc"
    
20 endif
    
21 
    22 if !exists('g:vviki_index')
    
23     " The start document for wiki root and subdirectories
    
24     " index + ext is the start filename (e.g. index.adoc)
    
25     let g:vviki_index = "index"
    
26 endif
    
27 
    28 if !exists('g:vviki_conceal_links')
    
29     " Use Vim's syntax concealing to temporarily hide link syntax
    
30     let g:vviki_conceal_links = 1
    
31 endif
    
32 
    33 if !exists('g:vviki_page_link_syntax')
    
34     " Set internal wiki page link syntax to one of:
    
35     "   'link'        ->   link:foo[My Foo]
    
36     "   'olink'       ->   olink:foo[My Foo]
    
37     "   'xref_hack'   ->   <<foo#,My Foo>>
    
38     let g:vviki_page_link_syntax = 'link'
    
39 endif
    
40 
    41 if !exists('g:vviki_visual_link_creation')
    
42     " Allow link creation from selected text in visual mode
    
43     let g:vviki_visual_link_creation = 0
    
44 endif
    
45 
    46 if !exists('g:vviki_links_include_ext')
    
47     " Internal wiki page links include the file extension.
    
48     " (File extension is set via g:vviki_ext.)
    
49     let g:vviki_links_include_ext = 0
    
50 endif
    
51 
    52 if !exists('g:vviki_custom_uri_function')
    
53     " Set to a custom function call to generate uri.
    
54     " If unset the URI will be equal to the selection.
    
55     " Make sure your function actually generates unique URIs
    
56     let g:vviki_custom_uri_function = ''
    
57 endif
    
58 
    59 " Navigation history for Backspace
    
60 let s:history = []
    
61 
    62 
    63 " Supported link styles:
    
64 function! VVEnter()
    
65     " Attempt to match existing link under cursor, trying all link syntax
    
66     " types (this intentionally ignores g:vviki_page_link_syntax).
    
67 
    68 	" Try to get path from AsciiDoc 'link' macro
    
69 	"   link:http://example.com[Example] - external
    
70 	"   link:page[My Page]               - internal relative page
    
71 	"   link:/page[My Page]              - internal absolute path to page
    
72 	"   link:../page[My Page]            - internal relative path to page
    
73     let l:linkpath = VVGetLink()
    
74 	if strlen(l:linkpath) > 0
    
75         echom "link:".l:linkpath
    
76 		if l:linkpath =~ '^https\?://'
    
77 			call VVGoUrl(l:linkpath)
    
78 		else
    
79 			call VVGoPath(l:linkpath)
    
80 		endif
    
81 		return
    
82 	end
    
83 
    84 	" Get path from AsciiDoc 'olink' macro (anticipating future support)
    
85 	"   olink:page[My Page]    - internal relative page
    
86 	"   olink:../page[My Page] - internal relative path to page
    
87     let l:linkpath = VVGetOLink()
    
88 	if strlen(l:linkpath) > 0
    
89         echom "olink:".l:linkpath
    
90         call VVGoPath(l:linkpath)
    
91 		return
    
92 	end
    
93 
    94 	" Get path from AsciiDoc '<<xref#>>' macro (for AsciiDoctor export)
    
95 	"   <<page#,My Page>>    - internal relative page
    
96 	"   <<../page#,My Page>> - internal relative path to page
    
97     let l:linkpath = VVGetXrefHack()
    
98 	if strlen(l:linkpath) > 0
    
99         echom "xrefhack:".l:linkpath
   
100         call VVGoPath(l:linkpath)
   
101 		return
   
102 	end
   
103 
   104 
   105 	" Did not match a link macro. Now there are three possibilities:
   
106 	"   1. We are on whitespace
   
107 	"   2. We are on a bare URL (http://...)
   
108 	"   3. We are on an unlinked word
   
109 	let l:whole_word = expand("<cWORD>") " selects all non-whitespace chars
   
110 	let l:word = expand("<cword>") " selects only 'word' chars
   
111 
   112     " Cursor on whitespace
   
113 	if l:whole_word == ''
   
114 		return
   
115 	endif
   
116 
   117     " Cursor on bare URL
   
118 	if l:whole_word =~ '^https\?://'
   
119 		call VVGoUrl(l:whole_word)
   
120 		return
   
121 	endif
   
122 
   123 	" Cursor on unlinked word - make it a link!
   
124     let l:new_link = VVMakeLink(l:word, l:word)
   
125 	execute "normal! ciw".l:new_link."\<ESC>"
   
126 endfunction
   
127 
   128 
   129 function! VVVisualEnter()
   
130     " Creates a new page link using whatever text is visually selected.
   
131     " Yank selection, replace with link, restore default register
   
132     let previous_register_contents = getreg('"')
   
133     normal! gvy
   
134     let user_selection = getreg('"')
   
135     let l:link = VVMakeLink(user_selection, user_selection)
   
136     normal! gvc
   
137     execute "normal! a" . l:link
   
138     call setreg('"', previous_register_contents)
   
139 endfunction
   
140 
   141 
   142 function! VVGetLink()
   
143 	" Captures the <path> portion of 'link:<path>[description]' (if any)
   
144     " \< is Vim regex for word start boundary
   
145     return VVGetMatchUnderCursor('\<link:\([^[]\+\)\[[^]]\+\]')
   
146 endfunction
   
147 
   148 
   149 function! VVGetOLink()
   
150 	" Captures the <path> portion of 'olink:<path>[description]' (if any)
   
151     " \< is Vim regex for word start boundary
   
152     return VVGetMatchUnderCursor('\<olink:\([^[]\+\)\[[^]]\+\]')
   
153 endfunction
   
154 
   155 
   156 function! VVGetXrefHack()
   
157 	" Captures the <path> portion of '<<<path>#,description>>' (if any)
   
158     return VVGetMatchUnderCursor('<<\([^#]\+\)#,[^>]\+>>')
   
159 endfunction
   
160 
   161 
   162 function! VVGetMatchUnderCursor(matchrx)
   
163     " Grab cursor pos and current line contents
   
164     let l:cursor = col('.')
   
165     let l:linestr = getline('.')
   
166 
   167     " Loop through the regex matches on the line, see if our cursor
   
168     " is inside one of them. If so, return it.
   
169     let l:matchstart=0
   
170     let l:matchend=0
   
171     while 1
   
172         " Note: match() always functions as if pattern were in 'magic' mode!
   
173         let l:matchstart =     match(l:linestr, a:matchrx, l:matchend)
   
174 		let l:matched    = matchlist(l:linestr, a:matchrx, l:matchend)
   
175         let l:matchend   =  matchend(l:linestr, a:matchrx, l:matchend)
   
176 
   177         " No match found or we're already past the cursor; done looking
   
178         if l:matchstart == -1 || l:matchstart > l:cursor
   
179             return ""
   
180         endif
   
181 
   182         if l:matchstart <= l:cursor && l:cursor <= l:matchend
   
183 			return l:matched[1]
   
184         endif
   
185     endwhile
   
186 endfunction
   
187 
   188 
   189 function! VVMakeLink(uri, description)
   
190     " Returns string with link of desired AsciiDoc syntax 'style'
   
191     let l:uri = a:uri
   
192 
   193     if g:vviki_custom_uri_function != ''
   
194         let l:uri = call(g:vviki_custom_uri_function, [])
   
195     endif
   
196     if g:vviki_links_include_ext
   
197         " Attach the wiki file extension to the link URI
   
198         let l:uri = l:uri.g:vviki_ext
   
199     endif
   
200     if g:vviki_page_link_syntax == 'link'
   
201         return "link:".l:uri."[".a:description."]"
   
202     elseif g:vviki_page_link_syntax == 'olink'
   
203         return "olink:".l:uri."[".a:description."]"
   
204     elseif g:vviki_page_link_syntax == 'xref_hack'
   
205         return "<<".l:uri."#,".a:description.">>"
   
206     endif
   
207 endfunction
   
208 
   209 
   210 function! VVFindNextLink()
   
211     " Places cursor on next link of desired AsciiDoc syntax
   
212     if g:vviki_page_link_syntax == 'link'
   
213         call search('link:.\{-1,}]')
   
214     elseif g:vviki_page_link_syntax == 'olink'
   
215         call search('olink:.\{-1,}]')
   
216     elseif g:vviki_page_link_syntax == 'xref_hack'
   
217         call search('<<.\{-1,}#,.\{-1,}>>')
   
218     endif
   
219 endfunction
   
220 
   221 
   222 function! VVFindPreviousLink()
   
223     " Places cursor on next link of desired AsciiDoc syntax
   
224     if g:vviki_page_link_syntax == 'link'
   
225         call search('link:.\{-1,}]', 'b')
   
226     elseif g:vviki_page_link_syntax == 'olink'
   
227         call search('olink:.\{-1,}]', 'b')
   
228     elseif g:vviki_page_link_syntax == 'xref_hack'
   
229         call search('<<.\{-1,}#,.\{-1,}>>', 'b')
   
230     endif
   
231 endfunction
   
232 
   233 
   234 function! VVGoPath(path)
   
235     " Push current page onto history
   
236     call add(s:history, expand("%:p"))
   
237 
   238     let l:fname = a:path
   
239 
   240     if l:fname =~ '/$'
   
241         " Path points to a directory, append default 'index' page
   
242         let l:fname = l:fname.g:vviki_index
   
243     end
   
244 
   245     " fname will no longer change, we can add extension here
   
246     if !g:vviki_links_include_ext
   
247         " Links don't already include extension, add it
   
248         let l:fname = l:fname.g:vviki_ext
   
249     endif
   
250 
   251     if l:fname =~ '^/'
   
252         " Path absolute from wiki root
   
253         let l:fname = g:vviki_root."/".l:fname
   
254     else
   
255         " Path relative to current page
   
256         let l:fname = expand("%:p:h")."/".l:fname
   
257     endif
   
258 
   259     let l:fname = fnameescape(l:fname)
   
260 
   261     execute "edit ".l:fname
   
262 endfunction
   
263 
   264 
   265 function! VVGoUrl(url)
   
266 	call system('xdg-open '.shellescape(a:url).' &')
   
267 endfunction
   
268 
   269 
   270 function! VVBack()
   
271 	if len(s:history) < 1
   
272 		return
   
273 	endif
   
274 
   275 	let l:last = remove(s:history, -1)
   
276 	execute "edit ".fnameescape(l:last)
   
277 endfunction
   
278 
   279 
   280 function! VVConcealLinks()
   
281     " Conceal the AsciiDoc link syntax until the cursor enters the line.
   
282     set conceallevel=2
   
283 
   284     if g:vviki_page_link_syntax == 'link'
   
285         syntax region vvikiLink start=/link:/ end=/\]/ keepend
   
286         syntax match vvikiLinkGuts /link:[^[]\+\[/ containedin=vvikiLink contained conceal
   
287         syntax match vvikiLinkGuts /\]/ containedin=vvikiLink contained conceal
   
288     elseif g:vviki_page_link_syntax == 'olink'
   
289         syntax region vvikiLink start=/olink:/ end=/\]/ keepend
   
290         syntax match vvikiLinkGuts /olink:[^[]\+\[/ containedin=vvikiLink contained conceal
   
291         syntax match vvikiLinkGuts /\]/ containedin=vvikiLink contained conceal
   
292     elseif g:vviki_page_link_syntax == 'xref_hack'
   
293         syntax region vvikiLink start=/<</ end=/>>/ keepend
   
294         syntax match vvikiLinkGuts /<<[^>]\+#,/ containedin=vvikiLink contained conceal
   
295         syntax match vvikiLinkGuts />>/ containedin=vvikiLink contained conceal
   
296     endif
   
297 
   298     highlight link vvikiLink Macro
   
299     highlight link vvikiLinkGuts Comment
   
300 endfunction
   
301 
   302 
   303 function! VVSetup()
   
304 	" Set wiki pages to automatically save
   
305 	set autowriteall
   
306 
   307 	" Map ENTER key to create/follow links
   
308 	nnoremap <buffer><silent> <CR> :call VVEnter()<CR>
   
309 
   310 	" Map BACKSPACE key to go back in history
   
311 	nnoremap <buffer><silent> <BS> :call VVBack()<CR>
   
312 
   313     " Map TAB key to find next link in page
   
314     " NOTE: search() always uses 'magic' regexp mode.
   
315     "       \{-1,} is Vim for match at least 1, non-greedy
   
316     nnoremap <buffer><silent> <TAB> :call VVFindNextLink()<CR>
   
317     " And backwards!
   
318     nnoremap <buffer><silent> <S-Tab> :call VVFindPreviousLink()<CR>
   
319 
   320     if g:vviki_visual_link_creation
   
321         vnoremap <buffer><silent> <CR> :call VVVisualEnter()<CR>
   
322     endif
   
323 
   324     if g:vviki_conceal_links
   
325         call VVConcealLinks()
   
326     endif
   
327 endfunction
   
328 
   329 function! VVShowHistory()
   
330     echo s:history
   
331 endfunction
   
332 
   333 " Detect wiki page
   
334 " If a buffer has the right parent directory and extension,
   
335 " map VViki keyboard shortcuts, etc.
   
336 augroup vviki
   
337 	au!
   
338 	execute "au BufNewFile,BufRead ".g:vviki_root."/*".g:vviki_ext." call VVSetup()"
   
339 augroup END
   
340