]> git.madduck.net Git - etc/vim.git/blob - ftplugin/mkd.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

CONTRIBUTING.md: add a section about issue reporting
[etc/vim.git] / ftplugin / mkd.vim
1 "TODO print messages when on visual mode. I only see VISUAL, not the messages.
2
3 " Function interface phylosophy:
4 "
5 " - functions take arbitrary line numbers as parameters.
6 "    Current cursor line is only a suitable default parameter.
7 "
8 " - only functions that bind directly to user actions:
9 "
10 "    - print error messages.
11 "       All intermediate functions limit themselves return `0` to indicate an error.
12 "
13 "    - move the cursor. All other functions do not move the cursor.
14 "
15 " This is how you should view headers for the header mappings:
16 "
17 "   |BUFFER
18 "   |
19 "   |Outside any header
20 "   |
21 " a-+# a
22 "   |
23 "   |Inside a
24 "   |
25 " a-+
26 " b-+## b
27 "   |
28 "   |inside b
29 "   |
30 " b-+
31 " c-+### c
32 "   |
33 "   |Inside c
34 "   |
35 " c-+
36 " d-|# d
37 "   |
38 "   |Inside d
39 "   |
40 " d-+
41 " e-|e
42 "   |====
43 "   |
44 "   |Inside e
45 "   |
46 " e-+
47
48 " For each level, contains the regexp that matches at that level only.
49 "
50 let s:levelRegexpDict = {
51     \ 1: '\v^(#[^#]@=|.+\n\=+$)',
52     \ 2: '\v^(##[^#]@=|.+\n-+$)',
53     \ 3: '\v^###[^#]@=',
54     \ 4: '\v^####[^#]@=',
55     \ 5: '\v^#####[^#]@=',
56     \ 6: '\v^######[^#]@='
57 \ }
58
59 " Maches any header level of any type.
60 "
61 " This could be deduced from `s:levelRegexpDict`, but it is more
62 " efficient to have a single regexp for this.
63 "
64 let s:headersRegexp = '\v^(#|.+\n(\=+|-+)$)'
65
66 " Returns the line number of the first header before `line`, called the
67 " current header.
68 "
69 " If there is no current header, return `0`.
70 "
71 " @param a:1 The line to look the header of. Default value: `getpos('.')`.
72 "
73 function! s:GetHeaderLineNum(...)
74     if a:0 == 0
75         let l:l = line('.')
76     else
77         let l:l = a:1
78     endif
79     while(l:l > 0)
80         if join(getline(l:l, l:l + 1), "\n") =~ s:headersRegexp
81             return l:l
82         endif
83         let l:l -= 1
84     endwhile
85     return 0
86 endfunction
87
88 " -  if inside a header goes to it.
89 "    Return its line number.
90 "
91 " -  if on top level outside any headers,
92 "    print a warning
93 "    Return `0`.
94 "
95 function! s:MoveToCurHeader()
96     let l:lineNum = s:GetHeaderLineNum()
97     if l:lineNum != 0
98         call cursor(l:lineNum, 1)
99     else
100         echo 'outside any header'
101         "normal! gg
102     endif
103     return l:lineNum
104 endfunction
105
106 " Move cursor to next header of any level.
107 "
108 " If there are no more headers, print a warning.
109 "
110 function! s:MoveToNextHeader()
111     if search(s:headersRegexp, 'W') == 0
112         "normal! G
113         echo 'no next header'
114     endif
115 endfunction
116
117 " Move cursor to previous header (before current) of any level.
118 "
119 " If it does not exist, print a warning.
120 "
121 function! s:MoveToPreviousHeader()
122     let l:curHeaderLineNumber = s:GetHeaderLineNum()
123     let l:noPreviousHeader = 0
124     if l:curHeaderLineNumber <= 1
125         let l:noPreviousHeader = 1
126     else
127         let l:previousHeaderLineNumber = s:GetHeaderLineNum(l:curHeaderLineNumber - 1)
128         if l:previousHeaderLineNumber == 0
129             let l:noPreviousHeader = 1
130         else
131             call cursor(l:previousHeaderLineNumber, 1)
132         endif
133     endif
134     if l:noPreviousHeader
135         echo 'no previous header'
136     endif
137 endfunction
138
139 " - if line is inside a header, return the header level (h1 -> 1, h2 -> 2, etc.).
140 "
141 " - if line is at top level outside any headers, return `0`.
142 "
143 function! s:GetHeaderLevel(...)
144     if a:0 == 0
145         let l:line = line('.')
146     else
147         let l:line = a:1
148     endif
149     let l:linenum = s:GetHeaderLineNum(l:line)
150     if l:linenum != 0
151         return s:GetLevelOfHeaderAtLine(l:linenum)
152     else
153         return 0
154     endif
155 endfunction
156
157 " Returns the level of the header at the given line.
158 "
159 " If there is no header at the given line, returns `0`.
160 "
161 function! s:GetLevelOfHeaderAtLine(linenum)
162     let l:lines = join(getline(a:linenum, a:linenum + 1), "\n")
163     for l:key in keys(s:levelRegexpDict)
164         if l:lines =~ get(s:levelRegexpDict, l:key)
165             return l:key
166         endif
167     endfor
168     return 0
169 endfunction
170
171 " Move cursor to parent header of the current header.
172 "
173 " If it does not exit, print a warning and do nothing.
174 "
175 function! s:MoveToParentHeader()
176     let l:linenum = s:GetParentHeaderLineNumber()
177     if l:linenum != 0
178         call cursor(l:linenum, 1)
179     else
180         echo 'no parent header'
181     endif
182 endfunction
183
184 " Return the line number of the parent header of line `line`.
185 "
186 " If it has no parent, return `0`.
187 "
188 function! s:GetParentHeaderLineNumber(...)
189     if a:0 == 0
190         let l:line = line('.')
191     else
192         let l:line = a:1
193     endif
194     let l:level = s:GetHeaderLevel(l:line)
195     if l:level > 1
196         let l:linenum = s:GetPreviousHeaderLineNumberAtLevel(l:level - 1, l:line)
197         return l:linenum
198     endif
199     return 0
200 endfunction
201
202 " Return the line number of the previous header of given level.
203 " in relation to line `a:1`. If not given, `a:1 = getline()`
204 "
205 " `a:1` line is included, and this may return the current header.
206 "
207 " If none return 0.
208 "
209 function! s:GetNextHeaderLineNumberAtLevel(level, ...)
210     if a:0 < 1
211         let l:line = line('.')
212     else
213         let l:line = a:1
214     endif
215     let l:l = l:line
216     while(l:l <= line('$'))
217         if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
218             return l:l
219         endif
220         let l:l += 1
221     endwhile
222     return 0
223 endfunction
224
225 " Return the line number of the previous header of given level.
226 " in relation to line `a:1`. If not given, `a:1 = getline()`
227 "
228 " `a:1` line is included, and this may return the current header.
229 "
230 " If none return 0.
231 "
232 function! s:GetPreviousHeaderLineNumberAtLevel(level, ...)
233     if a:0 == 0
234         let l:line = line('.')
235     else
236         let l:line = a:1
237     endif
238     let l:l = l:line
239     while(l:l > 0)
240         if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
241             return l:l
242         endif
243         let l:l -= 1
244     endwhile
245     return 0
246 endfunction
247
248 " Move cursor to next sibling header.
249 "
250 " If there is no next siblings, print a warning and don't move.
251 "
252 function! s:MoveToNextSiblingHeader()
253     let l:curHeaderLineNumber = s:GetHeaderLineNum()
254     let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
255     let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
256     let l:nextHeaderSameLevelLineNumber = s:GetNextHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber + 1)
257     let l:noNextSibling = 0
258     if l:nextHeaderSameLevelLineNumber == 0
259         let l:noNextSibling = 1
260     else
261         let l:nextHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:nextHeaderSameLevelLineNumber)
262         if l:curHeaderParentLineNumber == l:nextHeaderSameLevelParentLineNumber
263             call cursor(l:nextHeaderSameLevelLineNumber, 1)
264         else
265             let l:noNextSibling = 1
266         endif
267     endif
268     if l:noNextSibling
269         echo 'no next sibling header'
270     endif
271 endfunction
272
273 " Move cursor to previous sibling header.
274 "
275 " If there is no previous siblings, print a warning and do nothing.
276 "
277 function! s:MoveToPreviousSiblingHeader()
278     let l:curHeaderLineNumber = s:GetHeaderLineNum()
279     let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
280     let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
281     let l:previousHeaderSameLevelLineNumber = s:GetPreviousHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber - 1)
282     let l:noPreviousSibling = 0
283     if l:previousHeaderSameLevelLineNumber == 0
284         let l:noPreviousSibling = 1
285     else
286         let l:previousHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:previousHeaderSameLevelLineNumber)
287         if l:curHeaderParentLineNumber == l:previousHeaderSameLevelParentLineNumber
288             call cursor(l:previousHeaderSameLevelLineNumber, 1)
289         else
290             let l:noPreviousSibling = 1
291         endif
292     endif
293     if l:noPreviousSibling
294         echo 'no previous sibling header'
295     endif
296 endfunction
297
298 function! s:Toc(...)
299     if a:0 > 0
300         let l:window_type = a:1
301     else
302         let l:window_type = 'vertical'
303     endif
304
305     try
306         silent lvimgrep /\(^\S.*\(\n[=-]\+\n\)\@=\|^#\+\)/ %
307     catch /E480/
308         echom "Toc: No headers."
309         return
310     endtry
311
312     if l:window_type ==# 'horizontal'
313         lopen
314     elseif l:window_type ==# 'vertical'
315         vertical lopen
316         let &winwidth=(&columns/2)
317     elseif l:window_type ==# 'tab'
318         tab lopen
319     else
320         lopen
321     endif
322     setlocal modifiable
323     for i in range(1, line('$'))
324         " this is the location-list data for the current item
325         let d = getloclist(0)[i-1]
326         " atx headers
327         if match(d.text, "^#") > -1
328             let l:level = len(matchstr(d.text, '#*', 'g'))-1
329             let d.text = substitute(d.text, '\v^#*[ ]*', '', '')
330             let d.text = substitute(d.text, '\v[ ]*#*$', '', '')
331         " setex headers
332         else
333             let l:next_line = getbufline(bufname(d.bufnr), d.lnum+1)
334             if match(l:next_line, "=") > -1
335                 let l:level = 0
336             elseif match(l:next_line, "-") > -1
337                 let l:level = 1
338             endif
339         endif
340         call setline(i, repeat('  ', l:level). d.text)
341     endfor
342     setlocal nomodified
343     setlocal nomodifiable
344     normal! gg
345 endfunction
346
347 " Convert Setex headers in range `line1 .. line2` to Atx.
348 "
349 " Return the number of conversions.
350 "
351 function! s:SetexToAtx(line1, line2)
352     let l:originalNumLines = line('$')
353     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n\=+$/# \1/'
354     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n-+$/## \1/'
355     return l:originalNumLines - line('$')
356 endfunction
357
358 " If `a:1` is 0, decrease the level of all headers in range `line1 .. line2`.
359 "
360 " Otherwise, increase the level. `a:1` defaults to `0`.
361 "
362 function! s:HeaderDecrease(line1, line2, ...)
363     if a:0 > 0
364         let l:increase = a:1
365     else
366         let l:increase = 0
367     endif
368     if l:increase
369         let l:forbiddenLevel = 6
370         let l:replaceLevels = [5, 1]
371         let l:levelDelta = 1
372     else
373         let l:forbiddenLevel = 1
374         let l:replaceLevels = [2, 6]
375         let l:levelDelta = -1
376     endif
377     for l:line in range(a:line1, a:line2)
378         if join(getline(l:line, l:line + 1), "\n") =~ s:levelRegexpDict[l:forbiddenLevel]
379             echomsg 'There is an h' . l:forbiddenLevel . ' at line ' . l:line . '. Aborting.'
380             return
381         endif
382     endfor
383     let l:numSubstitutions = s:SetexToAtx(a:line1, a:line2)
384     for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
385         execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/g'
386     endfor
387 endfunction
388
389 " Format table under cursor.
390 "
391 " Depends on Tabularize.
392 "
393 function! s:TableFormat()
394     let l:pos = getpos('.')
395     normal! {
396     " Search instead of `normal! j` because of the table at beginning of file edge case.
397     call search('|')
398     normal! j
399     " Remove everything that is not a pipe othewise well formated tables would grow
400     " because of addition of 2 spaces on the separator line by Tabularize /|.
401     s/[^|]//g
402     Tabularize /|
403     s/ /-/g
404     call setpos('.', l:pos)
405 endfunction
406
407 " Wrapper to do move commands in visual mode.
408 "
409 function! s:VisMove(f)
410     norm! gv
411     call function(a:f)()
412 endfunction
413
414 " Map in both normal and visual modes.
415 "
416 function! s:MapNormVis(rhs,lhs)
417     execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
418     execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
419 endfunction
420
421 " Parameters:
422 "
423 " - step +1 for right, -1 for left
424 "
425 " TODO: multiple lines.
426 "
427 function! s:FindCornerOfSyntax(lnum, col, step)
428     let l:col = a:col
429     let l:syn = synIDattr(synID(a:lnum, l:col, 1), 'name')
430     while synIDattr(synID(a:lnum, l:col, 1), 'name') ==# l:syn
431         let l:col += a:step
432     endwhile
433     return l:col - a:step
434 endfunction
435
436 " Return the next position of the given syntax name,
437 " inclusive on the given position.
438 "
439 " TODO: multiple lines
440 "
441 function! s:FindNextSyntax(lnum, col, name)
442     let l:col = a:col
443     let l:step = 1
444     while synIDattr(synID(a:lnum, l:col, 1), 'name') !=# a:name
445         let l:col += l:step
446     endwhile
447     return [a:lnum, l:col]
448 endfunction
449
450 function! s:FindCornersOfSyntax(lnum, col)
451     return [<sid>FindLeftOfSyntax(a:lnum, a:col), <sid>FindRightOfSyntax(a:lnum, a:col)]
452 endfunction
453
454 function! s:FindRightOfSyntax(lnum, col)
455     return <sid>FindCornerOfSyntax(a:lnum, a:col, 1)
456 endfunction
457
458 function! s:FindLeftOfSyntax(lnum, col)
459     return <sid>FindCornerOfSyntax(a:lnum, a:col, -1)
460 endfunction
461
462 " Returns:
463 "
464 " - a string with the the URL for the link under the cursor
465 " - an empty string if the cursor is not on a link
466 "
467 " TODO
468 "
469 " - multiline support
470 " - give an error if the separator does is not on a link
471 "
472 function! s:Markdown_GetUrlForPosition(lnum, col)
473     let l:lnum = a:lnum
474     let l:col = a:col
475     let l:syn = synIDattr(synID(l:lnum, l:col, 1), 'name')
476
477     if l:syn ==# 'mkdInlineURL' || l:syn ==# 'mkdURL' || l:syn ==# 'mkdLinkDefTarget'
478         " Do nothing.
479     elseif l:syn ==# 'mkdLink'
480         let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
481         let l:syn = 'mkdURL'
482     elseif l:syn ==# 'mkdDelimiter'
483         let l:line = getline(l:lnum)
484         let l:char = l:line[col - 1]
485         if l:char ==# '<'
486             let l:col += 1
487         elseif l:char ==# '>' || l:char ==# ')'
488             let l:col -= 1
489         elseif l:char ==# '[' || l:char ==# ']' || l:char ==# '('
490             let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
491         else
492             return ''
493         endif
494     else
495         return ''
496     endif
497
498     let [l:left, l:right] = <sid>FindCornersOfSyntax(l:lnum, l:col)
499     return getline(l:lnum)[l:left - 1 : l:right - 1]
500 endfunction
501
502 " Front end for GetUrlForPosition.
503 "
504 function! s:OpenUrlUnderCursor()
505     let l:url = s:Markdown_GetUrlForPosition(line('.'), col('.'))
506     if l:url != ''
507         call s:VersionAwareNetrwBrowseX(l:url)
508     else
509         echomsg 'The cursor is not on a link.'
510     endif
511 endfunction
512
513 function! s:VersionAwareNetrwBrowseX(url)
514     if has('patch-7.4.567')
515         call netrw#BrowseX(a:url, 0)
516     else
517         call netrw#NetrwBrowseX(a:url, 0)
518     endif
519 endf
520
521 function! s:MapNotHasmapto(lhs, rhs)
522     if !hasmapto('<Plug>' . a:rhs)
523         execute 'nmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
524         execute 'vmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
525     endif
526 endfunction
527
528 call <sid>MapNormVis('<Plug>Markdown_MoveToNextHeader', '<sid>MoveToNextHeader')
529 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousHeader', '<sid>MoveToPreviousHeader')
530 call <sid>MapNormVis('<Plug>Markdown_MoveToNextSiblingHeader', '<sid>MoveToNextSiblingHeader')
531 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousSiblingHeader', '<sid>MoveToPreviousSiblingHeader')
532 call <sid>MapNormVis('<Plug>Markdown_MoveToParentHeader', '<sid>MoveToParentHeader')
533 call <sid>MapNormVis('<Plug>Markdown_MoveToCurHeader', '<sid>MoveToCurHeader')
534 nnoremap <Plug>Markdown_OpenUrlUnderCursor :call <sid>OpenUrlUnderCursor()<cr>
535
536 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
537     call <sid>MapNotHasmapto(']]', 'Markdown_MoveToNextHeader')
538     call <sid>MapNotHasmapto('[[', 'Markdown_MoveToPreviousHeader')
539     call <sid>MapNotHasmapto('][', 'Markdown_MoveToNextSiblingHeader')
540     call <sid>MapNotHasmapto('[]', 'Markdown_MoveToPreviousSiblingHeader')
541     call <sid>MapNotHasmapto(']u', 'Markdown_MoveToParentHeader')
542     call <sid>MapNotHasmapto(']c', 'Markdown_MoveToCurHeader')
543     call <sid>MapNotHasmapto('gx', 'Markdown_OpenUrlUnderCursor')
544 endif
545
546 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
547 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
548 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
549 command! -buffer TableFormat call s:TableFormat()
550 command! -buffer Toc call s:Toc()
551 command! -buffer Toch call s:Toc('horizontal')
552 command! -buffer Tocv call s:Toc('vertical')
553 command! -buffer Toct call s:Toc('tab')