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