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

Improve bold and italic tests.
[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     set modifiable
322     %s/\v^([^|]*\|){2,2} #//e
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     set nomodified
343     set nomodifiable
344     normal! gg
345 endfunction
346
347 " Wrapper to do move commands in visual mode.
348 "
349 function! s:VisMove(f)
350     norm! gv
351     call function(a:f)()
352 endfunction
353
354 " Map in both normal and visual modes.
355 "
356 function! s:MapNormVis(rhs,lhs)
357     execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
358     execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
359 endfunction
360
361 " Convert Setex headers in range `line1 .. line2` to Atx.
362 " Returns the number of conversions.
363 function! s:SetexToAtx(line1, line2)
364     let l:originalNumLines = line('$')
365     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n\=+$/# \1/'
366     execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n-+$/## \1/'
367     return l:originalNumLines - line('$')
368 endfunction
369
370 " If `a:1` is 0, decrease the level of all headers in range `line1 .. line2`.
371 " Otherwise, increase the level. `a:1` defaults to `0`.
372 function! s:HeaderDecrease(line1, line2, ...)
373     if a:0 > 0
374         let l:increase = a:1
375     else
376         let l:increase = 0
377     endif
378     if l:increase
379         let l:forbiddenLevel = 6
380         let l:replaceLevels = [5, 1]
381         let l:levelDelta = 1
382     else
383         let l:forbiddenLevel = 1
384         let l:replaceLevels = [2, 6]
385         let l:levelDelta = -1
386     endif
387     for l:line in range(a:line1, a:line2)
388         if join(getline(l:line, l:line + 1), "\n") =~ s:levelRegexpDict[l:forbiddenLevel]
389             echomsg 'There is an h' . l:forbiddenLevel . ' at line ' . l:line . '. Aborting.'
390             return
391         endif
392     endfor
393     let l:numSubstitutions = s:SetexToAtx(a:line1, a:line2)
394     for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
395         execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/g'
396     endfor
397 endfunction
398
399 " Format table under cursor.
400 " Depends on Tabularize.
401 function! s:TableFormat()
402   let l:pos = getpos('.')
403   normal! {
404   " Search instead of `normal! j` because of the table at beginning of file edge case.
405   call search('|')
406   normal! j
407   " Remove everything that is not a pipe othewise well formated tables would grow
408   " because of addition of 2 spaces on the separator line by Tabularize /|.
409   s/[^|]//g
410   Tabularize /|
411   s/ /-/g
412   call setpos('.', l:pos)
413 endfunction
414
415 call <sid>MapNormVis('<Plug>(Markdown_MoveToNextHeader)', '<sid>Markdown_MoveToNextHeader')
416 call <sid>MapNormVis('<Plug>(Markdown_MoveToPreviousHeader)', '<sid>Markdown_MoveToPreviousHeader')
417 call <sid>MapNormVis('<Plug>(Markdown_MoveToNextSiblingHeader)', '<sid>Markdown_MoveToNextSiblingHeader')
418 call <sid>MapNormVis('<Plug>(Markdown_MoveToPreviousSiblingHeader)', '<sid>Markdown_MoveToPreviousSiblingHeader')
419 " Menmonic: Up
420 call <sid>MapNormVis('<Plug>(Markdown_MoveToParentHeader)', '<sid>Markdown_MoveToParentHeader')
421 " Menmonic: Current
422 call <sid>MapNormVis('<Plug>(Markdown_MoveToCurHeader)', '<sid>Markdown_MoveToCurHeader')
423
424 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
425     nmap <buffer> ]] <Plug>(Markdown_MoveToNextHeader)
426     nmap <buffer> [[ <Plug>(Markdown_MoveToPreviousHeader)
427     nmap <buffer> ][ <Plug>(Markdown_MoveToNextSiblingHeader)
428     nmap <buffer> [] <Plug>(Markdown_MoveToPreviousSiblingHeader)
429     nmap <buffer> ]u <Plug>(Markdown_MoveToParentHeader)
430     nmap <buffer> ]c <Plug>(Markdown_MoveToCurHeader)
431
432     vmap <buffer> ]] <Plug>(Markdown_MoveToNextHeader)
433     vmap <buffer> [[ <Plug>(Markdown_MoveToPreviousHeader)
434     vmap <buffer> ][ <Plug>(Markdown_MoveToNextSiblingHeader)
435     vmap <buffer> [] <Plug>(Markdown_MoveToPreviousSiblingHeader)
436     vmap <buffer> ]u <Plug>(Markdown_MoveToParentHeader)
437     vmap <buffer> ]c <Plug>(Markdown_MoveToCurHeader)
438 endif
439
440 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
441 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
442 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
443 command! -buffer TableFormat call s:TableFormat()
444 command! -buffer Toc call s:Markdown_Toc()
445 command! -buffer Toch call s:Markdown_Toc('horizontal')
446 command! -buffer Tocv call s:Markdown_Toc('vertical')
447 command! -buffer Toct call s:Markdown_Toc('tab')