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

Add tilde-fenced code blocks.
[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     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     set nomodified
342     set 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 call <sid>MapNormVis('<Plug>(Markdown_MoveToNextHeader)', '<sid>Markdown_MoveToNextHeader')
415 call <sid>MapNormVis('<Plug>(Markdown_MoveToPreviousHeader)', '<sid>Markdown_MoveToPreviousHeader')
416 call <sid>MapNormVis('<Plug>(Markdown_MoveToNextSiblingHeader)', '<sid>Markdown_MoveToNextSiblingHeader')
417 call <sid>MapNormVis('<Plug>(Markdown_MoveToPreviousSiblingHeader)', '<sid>Markdown_MoveToPreviousSiblingHeader')
418 " Menmonic: Up
419 call <sid>MapNormVis('<Plug>(Markdown_MoveToParentHeader)', '<sid>Markdown_MoveToParentHeader')
420 " Menmonic: Current
421 call <sid>MapNormVis('<Plug>(Markdown_MoveToCurHeader)', '<sid>Markdown_MoveToCurHeader')
422
423 if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
424     nmap <buffer> ]] <Plug>(Markdown_MoveToNextHeader)
425     nmap <buffer> [[ <Plug>(Markdown_MoveToPreviousHeader)
426     nmap <buffer> ][ <Plug>(Markdown_MoveToNextSiblingHeader)
427     nmap <buffer> [] <Plug>(Markdown_MoveToPreviousSiblingHeader)
428     nmap <buffer> ]u <Plug>(Markdown_MoveToParentHeader)
429     nmap <buffer> ]c <Plug>(Markdown_MoveToCurHeader)
430
431     vmap <buffer> ]] <Plug>(Markdown_MoveToNextHeader)
432     vmap <buffer> [[ <Plug>(Markdown_MoveToPreviousHeader)
433     vmap <buffer> ][ <Plug>(Markdown_MoveToNextSiblingHeader)
434     vmap <buffer> [] <Plug>(Markdown_MoveToPreviousSiblingHeader)
435     vmap <buffer> ]u <Plug>(Markdown_MoveToParentHeader)
436     vmap <buffer> ]c <Plug>(Markdown_MoveToCurHeader)
437 endif
438
439 command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
440 command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
441 command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
442 command! -buffer TableFormat call s:TableFormat()
443 command! -buffer Toc call s:Markdown_Toc()
444 command! -buffer Toch call s:Markdown_Toc('horizontal')
445 command! -buffer Tocv call s:Markdown_Toc('vertical')
446 command! -buffer Toct call s:Markdown_Toc('tab')