]> git.madduck.net Git - etc/vim.git/blob - ftplugin/markdown.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:

bc916eff7b57700c58ee36d4bd6f17c8b4cc0c55
[etc/vim.git] / ftplugin / markdown.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
306     let b:bufnr = bufnr('%')
307     let b:fenced_block = 0
308     let b:header_list = []
309     let l:header_max_len = 0
310     for i in range(1, line('$'))
311         let l:lineraw = getline(i)
312         let l:line = substitute(l:lineraw, "#", "\\\#", "g")
313         if l:line =~ '````*' || l:line =~ '\~\~\~\~*'
314             if b:fenced_block == 0
315                 let b:fenced_block = 1
316             elseif b:fenced_block == 1
317                 let b:fenced_block = 0
318             endif
319         endif
320         if l:line =~ '^#\+' && b:fenced_block == 0
321             " append line to location list
322             let b:item = {'lnum': i, 'text': l:line, 'valid': 1, 'bufnr': b:bufnr, 'col': 1}
323             let b:header_list = b:header_list + [b:item]
324         endif
325     endfor
326     if len(b:header_list) == 0
327         echom "Toc: No headers."
328         return
329     endif
330     call setloclist(0, b:header_list)
331
332     if l:window_type ==# 'horizontal'
333         lopen
334     elseif l:window_type ==# 'vertical'
335         vertical lopen
336         let &winwidth=(&columns/2)
337     elseif l:window_type ==# 'tab'
338         tab lopen
339     else
340         lopen
341     endif
342     setlocal modifiable
343     for i in range(1, line('$'))
344         " this is the location-list data for the current item
345         let d = getloclist(0)[i-1]
346         " atx headers
347         if match(d.text, "^#") > -1
348             let l:level = len(matchstr(d.text, '#*', 'g'))-1
349             let d.text = substitute(d.text, '\v^#*[ ]*', '', '')
350             let d.text = substitute(d.text, '\v[ ]*#*$', '', '')
351         " setex headers
352         else
353             let l:next_line = getbufline(bufname(d.bufnr), d.lnum+1)
354             if match(l:next_line, "=") > -1
355                 let l:level = 0
356             elseif match(l:next_line, "-") > -1
357                 let l:level = 1
358             endif
359         endif
360         call setline(i, repeat('  ', l:level). d.text)
361     endfor
362     setlocal nomodified
363     setlocal nomodifiable
364     normal! gg
365 endfunction
366
367 " Convert Setex headers in range `line1 .. line2` to Atx.
368 "
369 " Return the number of conversions.
370 "
371 function! s:SetexToAtx(line1, line2)
372     let l:originalNumLines = line('$')
373     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n\=+$/# \1/'
374     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n-+$/## \1/'
375     return l:originalNumLines - line('$')
376 endfunction
377
378 " If `a:1` is 0, decrease the level of all headers in range `line1 .. line2`.
379 "
380 " Otherwise, increase the level. `a:1` defaults to `0`.
381 "
382 function! s:HeaderDecrease(line1, line2, ...)
383     if a:0 > 0
384         let l:increase = a:1
385     else
386         let l:increase = 0
387     endif
388     if l:increase
389         let l:forbiddenLevel = 6
390         let l:replaceLevels = [5, 1]
391         let l:levelDelta = 1
392     else
393         let l:forbiddenLevel = 1
394         let l:replaceLevels = [2, 6]
395         let l:levelDelta = -1
396     endif
397     for l:line in range(a:line1, a:line2)
398         if join(getline(l:line, l:line + 1), "\n") =~ s:levelRegexpDict[l:forbiddenLevel]
399             echomsg 'There is an h' . l:forbiddenLevel . ' at line ' . l:line . '. Aborting.'
400             return
401         endif
402     endfor
403     let l:numSubstitutions = s:SetexToAtx(a:line1, a:line2)
404     let l:flags = (&gdefault ? '' : 'g')
405     for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
406         execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/' . l:flags
407     endfor
408 endfunction
409
410 " Format table under cursor.
411 "
412 " Depends on Tabularize.
413 "
414 function! s:TableFormat()
415     let l:pos = getpos('.')
416     normal! {
417     " Search instead of `normal! j` because of the table at beginning of file edge case.
418     call search('|')
419     normal! j
420     " Remove everything that is not a pipe othewise well formated tables would grow
421     " because of addition of 2 spaces on the separator line by Tabularize /|.
422     let l:flags = (&gdefault ? '' : 'g')
423     execute 's/[^|]//' . l:flags
424     Tabularize /|
425     execute 's/ /-/' . l:flags
426     call setpos('.', l:pos)
427 endfunction
428
429 " Wrapper to do move commands in visual mode.
430 "
431 function! s:VisMove(f)
432     norm! gv
433     call function(a:f)()
434 endfunction
435
436 " Map in both normal and visual modes.
437 "
438 function! s:MapNormVis(rhs,lhs)
439     execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
440     execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
441 endfunction
442
443 " Parameters:
444 "
445 " - step +1 for right, -1 for left
446 "
447 " TODO: multiple lines.
448 "
449 function! s:FindCornerOfSyntax(lnum, col, step)
450     let l:col = a:col
451     let l:syn = synIDattr(synID(a:lnum, l:col, 1), 'name')
452     while synIDattr(synID(a:lnum, l:col, 1), 'name') ==# l:syn
453         let l:col += a:step
454     endwhile
455     return l:col - a:step
456 endfunction
457
458 " Return the next position of the given syntax name,
459 " inclusive on the given position.
460 "
461 " TODO: multiple lines
462 "
463 function! s:FindNextSyntax(lnum, col, name)
464     let l:col = a:col
465     let l:step = 1
466     while synIDattr(synID(a:lnum, l:col, 1), 'name') !=# a:name
467         let l:col += l:step
468     endwhile
469     return [a:lnum, l:col]
470 endfunction
471
472 function! s:FindCornersOfSyntax(lnum, col)
473     return [<sid>FindLeftOfSyntax(a:lnum, a:col), <sid>FindRightOfSyntax(a:lnum, a:col)]
474 endfunction
475
476 function! s:FindRightOfSyntax(lnum, col)
477     return <sid>FindCornerOfSyntax(a:lnum, a:col, 1)
478 endfunction
479
480 function! s:FindLeftOfSyntax(lnum, col)
481     return <sid>FindCornerOfSyntax(a:lnum, a:col, -1)
482 endfunction
483
484 " Returns:
485 "
486 " - a string with the the URL for the link under the cursor
487 " - an empty string if the cursor is not on a link
488 "
489 " TODO
490 "
491 " - multiline support
492 " - give an error if the separator does is not on a link
493 "
494 function! s:Markdown_GetUrlForPosition(lnum, col)
495     let l:lnum = a:lnum
496     let l:col = a:col
497     let l:syn = synIDattr(synID(l:lnum, l:col, 1), 'name')
498
499     if l:syn ==# 'mkdInlineURL' || l:syn ==# 'mkdURL' || l:syn ==# 'mkdLinkDefTarget'
500         " Do nothing.
501     elseif l:syn ==# 'mkdLink'
502         let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
503         let l:syn = 'mkdURL'
504     elseif l:syn ==# 'mkdDelimiter'
505         let l:line = getline(l:lnum)
506         let l:char = l:line[col - 1]
507         if l:char ==# '<'
508             let l:col += 1
509         elseif l:char ==# '>' || l:char ==# ')'
510             let l:col -= 1
511         elseif l:char ==# '[' || l:char ==# ']' || l:char ==# '('
512             let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
513         else
514             return ''
515         endif
516     else
517         return ''
518     endif
519
520     let [l:left, l:right] = <sid>FindCornersOfSyntax(l:lnum, l:col)
521     return getline(l:lnum)[l:left - 1 : l:right - 1]
522 endfunction
523
524 " Front end for GetUrlForPosition.
525 "
526 function! s:OpenUrlUnderCursor()
527     let l:url = s:Markdown_GetUrlForPosition(line('.'), col('.'))
528     if l:url != ''
529         call s:VersionAwareNetrwBrowseX(l:url)
530     else
531         echomsg 'The cursor is not on a link.'
532     endif
533 endfunction
534
535 function! s:VersionAwareNetrwBrowseX(url)
536     if has('patch-7.4.567')
537         call netrw#BrowseX(a:url, 0)
538     else
539         call netrw#NetrwBrowseX(a:url, 0)
540     endif
541 endf
542
543 function! s:MapNotHasmapto(lhs, rhs)
544     if !hasmapto('<Plug>' . a:rhs)
545         execute 'nmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
546         execute 'vmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
547     endif
548 endfunction
549
550 call <sid>MapNormVis('<Plug>Markdown_MoveToNextHeader', '<sid>MoveToNextHeader')
551 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousHeader', '<sid>MoveToPreviousHeader')
552 call <sid>MapNormVis('<Plug>Markdown_MoveToNextSiblingHeader', '<sid>MoveToNextSiblingHeader')
553 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousSiblingHeader', '<sid>MoveToPreviousSiblingHeader')
554 call <sid>MapNormVis('<Plug>Markdown_MoveToParentHeader', '<sid>MoveToParentHeader')
555 call <sid>MapNormVis('<Plug>Markdown_MoveToCurHeader', '<sid>MoveToCurHeader')
556 nnoremap <Plug>Markdown_OpenUrlUnderCursor :call <sid>OpenUrlUnderCursor()<cr>
557
558 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
559     call <sid>MapNotHasmapto(']]', 'Markdown_MoveToNextHeader')
560     call <sid>MapNotHasmapto('[[', 'Markdown_MoveToPreviousHeader')
561     call <sid>MapNotHasmapto('][', 'Markdown_MoveToNextSiblingHeader')
562     call <sid>MapNotHasmapto('[]', 'Markdown_MoveToPreviousSiblingHeader')
563     call <sid>MapNotHasmapto(']u', 'Markdown_MoveToParentHeader')
564     call <sid>MapNotHasmapto(']c', 'Markdown_MoveToCurHeader')
565     call <sid>MapNotHasmapto('gx', 'Markdown_OpenUrlUnderCursor')
566 endif
567
568 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
569 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
570 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
571 command! -buffer TableFormat call s:TableFormat()
572 command! -buffer Toc call s:Toc()
573 command! -buffer Toch call s:Toc('horizontal')
574 command! -buffer Tocv call s:Toc('vertical')
575 command! -buffer Toct call s:Toc('tab')