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

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