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

Remove needless + from setext-style headers tests
[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     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     let l:flags = (&gdefault ? '' : 'g')
385     for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
386         execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/' . l:flags
387     endfor
388 endfunction
389
390 " Format table under cursor.
391 "
392 " Depends on Tabularize.
393 "
394 function! s:TableFormat()
395     let l:pos = getpos('.')
396     normal! {
397     " Search instead of `normal! j` because of the table at beginning of file edge case.
398     call search('|')
399     normal! j
400     " Remove everything that is not a pipe othewise well formated tables would grow
401     " because of addition of 2 spaces on the separator line by Tabularize /|.
402     let l:flags = (&gdefault ? '' : 'g')
403     execute 's/[^|]//' . l:flags
404     Tabularize /|
405     execute 's/ /-/' . l:flags
406     call setpos('.', l:pos)
407 endfunction
408
409 " Wrapper to do move commands in visual mode.
410 "
411 function! s:VisMove(f)
412     norm! gv
413     call function(a:f)()
414 endfunction
415
416 " Map in both normal and visual modes.
417 "
418 function! s:MapNormVis(rhs,lhs)
419     execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
420     execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
421 endfunction
422
423 " Parameters:
424 "
425 " - step +1 for right, -1 for left
426 "
427 " TODO: multiple lines.
428 "
429 function! s:FindCornerOfSyntax(lnum, col, step)
430     let l:col = a:col
431     let l:syn = synIDattr(synID(a:lnum, l:col, 1), 'name')
432     while synIDattr(synID(a:lnum, l:col, 1), 'name') ==# l:syn
433         let l:col += a:step
434     endwhile
435     return l:col - a:step
436 endfunction
437
438 " Return the next position of the given syntax name,
439 " inclusive on the given position.
440 "
441 " TODO: multiple lines
442 "
443 function! s:FindNextSyntax(lnum, col, name)
444     let l:col = a:col
445     let l:step = 1
446     while synIDattr(synID(a:lnum, l:col, 1), 'name') !=# a:name
447         let l:col += l:step
448     endwhile
449     return [a:lnum, l:col]
450 endfunction
451
452 function! s:FindCornersOfSyntax(lnum, col)
453     return [<sid>FindLeftOfSyntax(a:lnum, a:col), <sid>FindRightOfSyntax(a:lnum, a:col)]
454 endfunction
455
456 function! s:FindRightOfSyntax(lnum, col)
457     return <sid>FindCornerOfSyntax(a:lnum, a:col, 1)
458 endfunction
459
460 function! s:FindLeftOfSyntax(lnum, col)
461     return <sid>FindCornerOfSyntax(a:lnum, a:col, -1)
462 endfunction
463
464 " Returns:
465 "
466 " - a string with the the URL for the link under the cursor
467 " - an empty string if the cursor is not on a link
468 "
469 " TODO
470 "
471 " - multiline support
472 " - give an error if the separator does is not on a link
473 "
474 function! s:Markdown_GetUrlForPosition(lnum, col)
475     let l:lnum = a:lnum
476     let l:col = a:col
477     let l:syn = synIDattr(synID(l:lnum, l:col, 1), 'name')
478
479     if l:syn ==# 'mkdInlineURL' || l:syn ==# 'mkdURL' || l:syn ==# 'mkdLinkDefTarget'
480         " Do nothing.
481     elseif l:syn ==# 'mkdLink'
482         let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
483         let l:syn = 'mkdURL'
484     elseif l:syn ==# 'mkdDelimiter'
485         let l:line = getline(l:lnum)
486         let l:char = l:line[col - 1]
487         if l:char ==# '<'
488             let l:col += 1
489         elseif l:char ==# '>' || l:char ==# ')'
490             let l:col -= 1
491         elseif l:char ==# '[' || l:char ==# ']' || l:char ==# '('
492             let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
493         else
494             return ''
495         endif
496     else
497         return ''
498     endif
499
500     let [l:left, l:right] = <sid>FindCornersOfSyntax(l:lnum, l:col)
501     return getline(l:lnum)[l:left - 1 : l:right - 1]
502 endfunction
503
504 " Front end for GetUrlForPosition.
505 "
506 function! s:OpenUrlUnderCursor()
507     let l:url = s:Markdown_GetUrlForPosition(line('.'), col('.'))
508     if l:url != ''
509         call s:VersionAwareNetrwBrowseX(l:url)
510     else
511         echomsg 'The cursor is not on a link.'
512     endif
513 endfunction
514
515 function! s:VersionAwareNetrwBrowseX(url)
516     if has('patch-7.4.567')
517         call netrw#BrowseX(a:url, 0)
518     else
519         call netrw#NetrwBrowseX(a:url, 0)
520     endif
521 endf
522
523 function! s:MapNotHasmapto(lhs, rhs)
524     if !hasmapto('<Plug>' . a:rhs)
525         execute 'nmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
526         execute 'vmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
527     endif
528 endfunction
529
530 call <sid>MapNormVis('<Plug>Markdown_MoveToNextHeader', '<sid>MoveToNextHeader')
531 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousHeader', '<sid>MoveToPreviousHeader')
532 call <sid>MapNormVis('<Plug>Markdown_MoveToNextSiblingHeader', '<sid>MoveToNextSiblingHeader')
533 call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousSiblingHeader', '<sid>MoveToPreviousSiblingHeader')
534 call <sid>MapNormVis('<Plug>Markdown_MoveToParentHeader', '<sid>MoveToParentHeader')
535 call <sid>MapNormVis('<Plug>Markdown_MoveToCurHeader', '<sid>MoveToCurHeader')
536 nnoremap <Plug>Markdown_OpenUrlUnderCursor :call <sid>OpenUrlUnderCursor()<cr>
537
538 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
539     call <sid>MapNotHasmapto(']]', 'Markdown_MoveToNextHeader')
540     call <sid>MapNotHasmapto('[[', 'Markdown_MoveToPreviousHeader')
541     call <sid>MapNotHasmapto('][', 'Markdown_MoveToNextSiblingHeader')
542     call <sid>MapNotHasmapto('[]', 'Markdown_MoveToPreviousSiblingHeader')
543     call <sid>MapNotHasmapto(']u', 'Markdown_MoveToParentHeader')
544     call <sid>MapNotHasmapto(']c', 'Markdown_MoveToCurHeader')
545     call <sid>MapNotHasmapto('gx', 'Markdown_OpenUrlUnderCursor')
546 endif
547
548 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
549 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
550 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
551 command! -buffer TableFormat call s:TableFormat()
552 command! -buffer Toc call s:Toc()
553 command! -buffer Toch call s:Toc('horizontal')
554 command! -buffer Tocv call s:Toc('vertical')
555 command! -buffer Toct call s:Toc('tab')