]> 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:

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