Markdown Vim Mode
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

553 lines
16 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. "TODO print messages when on visual mode. I only see VISUAL, not the messages.
  2. " Function interface phylosophy:
  3. "
  4. " - functions take arbitrary line numbers as parameters.
  5. " Current cursor line is only a suitable default parameter.
  6. "
  7. " - only functions that bind directly to user actions:
  8. "
  9. " - print error messages.
  10. " All intermediate functions limit themselves return `0` to indicate an error.
  11. "
  12. " - move the cursor. All other functions do not move the cursor.
  13. "
  14. " This is how you should view headers for the header mappings:
  15. "
  16. " |BUFFER
  17. " |
  18. " |Outside any header
  19. " |
  20. " a-+# a
  21. " |
  22. " |Inside a
  23. " |
  24. " a-+
  25. " b-+## b
  26. " |
  27. " |inside b
  28. " |
  29. " b-+
  30. " c-+### c
  31. " |
  32. " |Inside c
  33. " |
  34. " c-+
  35. " d-|# d
  36. " |
  37. " |Inside d
  38. " |
  39. " d-+
  40. " e-|e
  41. " |====
  42. " |
  43. " |Inside e
  44. " |
  45. " e-+
  46. " For each level, contains the regexp that matches at that level only.
  47. "
  48. let s:levelRegexpDict = {
  49. \ 1: '\v^(#[^#]@=|.+\n\=+$)',
  50. \ 2: '\v^(##[^#]@=|.+\n-+$)',
  51. \ 3: '\v^###[^#]@=',
  52. \ 4: '\v^####[^#]@=',
  53. \ 5: '\v^#####[^#]@=',
  54. \ 6: '\v^######[^#]@='
  55. \ }
  56. " Maches any header level of any type.
  57. "
  58. " This could be deduced from `s:levelRegexpDict`, but it is more
  59. " efficient to have a single regexp for this.
  60. "
  61. let s:headersRegexp = '\v^(#|.+\n(\=+|-+)$)'
  62. " Returns the line number of the first header before `line`, called the
  63. " current header.
  64. "
  65. " If there is no current header, return `0`.
  66. "
  67. " @param a:1 The line to look the header of. Default value: `getpos('.')`.
  68. "
  69. function! s:GetHeaderLineNum(...)
  70. if a:0 == 0
  71. let l:l = line('.')
  72. else
  73. let l:l = a:1
  74. endif
  75. while(l:l > 0)
  76. if join(getline(l:l, l:l + 1), "\n") =~ s:headersRegexp
  77. return l:l
  78. endif
  79. let l:l -= 1
  80. endwhile
  81. return 0
  82. endfunction
  83. " - if inside a header goes to it.
  84. " Return its line number.
  85. "
  86. " - if on top level outside any headers,
  87. " print a warning
  88. " Return `0`.
  89. "
  90. function! s:MoveToCurHeader()
  91. let l:lineNum = s:GetHeaderLineNum()
  92. if l:lineNum != 0
  93. call cursor(l:lineNum, 1)
  94. else
  95. echo 'outside any header'
  96. "normal! gg
  97. endif
  98. return l:lineNum
  99. endfunction
  100. " Move cursor to next header of any level.
  101. "
  102. " If there are no more headers, print a warning.
  103. "
  104. function! s:MoveToNextHeader()
  105. if search(s:headersRegexp, 'W') == 0
  106. "normal! G
  107. echo 'no next header'
  108. endif
  109. endfunction
  110. " Move cursor to previous header (before current) of any level.
  111. "
  112. " If it does not exist, print a warning.
  113. "
  114. function! s:MoveToPreviousHeader()
  115. let l:curHeaderLineNumber = s:GetHeaderLineNum()
  116. let l:noPreviousHeader = 0
  117. if l:curHeaderLineNumber <= 1
  118. let l:noPreviousHeader = 1
  119. else
  120. let l:previousHeaderLineNumber = s:GetHeaderLineNum(l:curHeaderLineNumber - 1)
  121. if l:previousHeaderLineNumber == 0
  122. let l:noPreviousHeader = 1
  123. else
  124. call cursor(l:previousHeaderLineNumber, 1)
  125. endif
  126. endif
  127. if l:noPreviousHeader
  128. echo 'no previous header'
  129. endif
  130. endfunction
  131. " - if line is inside a header, return the header level (h1 -> 1, h2 -> 2, etc.).
  132. "
  133. " - if line is at top level outside any headers, return `0`.
  134. "
  135. function! s:GetHeaderLevel(...)
  136. if a:0 == 0
  137. let l:line = line('.')
  138. else
  139. let l:line = a:1
  140. endif
  141. let l:linenum = s:GetHeaderLineNum(l:line)
  142. if l:linenum != 0
  143. return s:GetLevelOfHeaderAtLine(l:linenum)
  144. else
  145. return 0
  146. endif
  147. endfunction
  148. " Returns the level of the header at the given line.
  149. "
  150. " If there is no header at the given line, returns `0`.
  151. "
  152. function! s:GetLevelOfHeaderAtLine(linenum)
  153. let l:lines = join(getline(a:linenum, a:linenum + 1), "\n")
  154. for l:key in keys(s:levelRegexpDict)
  155. if l:lines =~ get(s:levelRegexpDict, l:key)
  156. return l:key
  157. endif
  158. endfor
  159. return 0
  160. endfunction
  161. " Move cursor to parent header of the current header.
  162. "
  163. " If it does not exit, print a warning and do nothing.
  164. "
  165. function! s:MoveToParentHeader()
  166. let l:linenum = s:GetParentHeaderLineNumber()
  167. if l:linenum != 0
  168. call cursor(l:linenum, 1)
  169. else
  170. echo 'no parent header'
  171. endif
  172. endfunction
  173. " Return the line number of the parent header of line `line`.
  174. "
  175. " If it has no parent, return `0`.
  176. "
  177. function! s:GetParentHeaderLineNumber(...)
  178. if a:0 == 0
  179. let l:line = line('.')
  180. else
  181. let l:line = a:1
  182. endif
  183. let l:level = s:GetHeaderLevel(l:line)
  184. if l:level > 1
  185. let l:linenum = s:GetPreviousHeaderLineNumberAtLevel(l:level - 1, l:line)
  186. return l:linenum
  187. endif
  188. return 0
  189. endfunction
  190. " Return the line number of the previous header of given level.
  191. " in relation to line `a:1`. If not given, `a:1 = getline()`
  192. "
  193. " `a:1` line is included, and this may return the current header.
  194. "
  195. " If none return 0.
  196. "
  197. function! s:GetNextHeaderLineNumberAtLevel(level, ...)
  198. if a:0 < 1
  199. let l:line = line('.')
  200. else
  201. let l:line = a:1
  202. endif
  203. let l:l = l:line
  204. while(l:l <= line('$'))
  205. if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
  206. return l:l
  207. endif
  208. let l:l += 1
  209. endwhile
  210. return 0
  211. endfunction
  212. " Return the line number of the previous header of given level.
  213. " in relation to line `a:1`. If not given, `a:1 = getline()`
  214. "
  215. " `a:1` line is included, and this may return the current header.
  216. "
  217. " If none return 0.
  218. "
  219. function! s:GetPreviousHeaderLineNumberAtLevel(level, ...)
  220. if a:0 == 0
  221. let l:line = line('.')
  222. else
  223. let l:line = a:1
  224. endif
  225. let l:l = l:line
  226. while(l:l > 0)
  227. if join(getline(l:l, l:l + 1), "\n") =~ get(s:levelRegexpDict, a:level)
  228. return l:l
  229. endif
  230. let l:l -= 1
  231. endwhile
  232. return 0
  233. endfunction
  234. " Move cursor to next sibling header.
  235. "
  236. " If there is no next siblings, print a warning and don't move.
  237. "
  238. function! s:MoveToNextSiblingHeader()
  239. let l:curHeaderLineNumber = s:GetHeaderLineNum()
  240. let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
  241. let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
  242. let l:nextHeaderSameLevelLineNumber = s:GetNextHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber + 1)
  243. let l:noNextSibling = 0
  244. if l:nextHeaderSameLevelLineNumber == 0
  245. let l:noNextSibling = 1
  246. else
  247. let l:nextHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:nextHeaderSameLevelLineNumber)
  248. if l:curHeaderParentLineNumber == l:nextHeaderSameLevelParentLineNumber
  249. call cursor(l:nextHeaderSameLevelLineNumber, 1)
  250. else
  251. let l:noNextSibling = 1
  252. endif
  253. endif
  254. if l:noNextSibling
  255. echo 'no next sibling header'
  256. endif
  257. endfunction
  258. " Move cursor to previous sibling header.
  259. "
  260. " If there is no previous siblings, print a warning and do nothing.
  261. "
  262. function! s:MoveToPreviousSiblingHeader()
  263. let l:curHeaderLineNumber = s:GetHeaderLineNum()
  264. let l:curHeaderLevel = s:GetLevelOfHeaderAtLine(l:curHeaderLineNumber)
  265. let l:curHeaderParentLineNumber = s:GetParentHeaderLineNumber()
  266. let l:previousHeaderSameLevelLineNumber = s:GetPreviousHeaderLineNumberAtLevel(l:curHeaderLevel, l:curHeaderLineNumber - 1)
  267. let l:noPreviousSibling = 0
  268. if l:previousHeaderSameLevelLineNumber == 0
  269. let l:noPreviousSibling = 1
  270. else
  271. let l:previousHeaderSameLevelParentLineNumber = s:GetParentHeaderLineNumber(l:previousHeaderSameLevelLineNumber)
  272. if l:curHeaderParentLineNumber == l:previousHeaderSameLevelParentLineNumber
  273. call cursor(l:previousHeaderSameLevelLineNumber, 1)
  274. else
  275. let l:noPreviousSibling = 1
  276. endif
  277. endif
  278. if l:noPreviousSibling
  279. echo 'no previous sibling header'
  280. endif
  281. endfunction
  282. function! s:Toc(...)
  283. if a:0 > 0
  284. let l:window_type = a:1
  285. else
  286. let l:window_type = 'vertical'
  287. endif
  288. try
  289. silent lvimgrep /\(^\S.*\(\n[=-]\+\n\)\@=\|^#\+\)/ %
  290. catch /E480/
  291. echom "Toc: No headers."
  292. return
  293. endtry
  294. if l:window_type ==# 'horizontal'
  295. lopen
  296. elseif l:window_type ==# 'vertical'
  297. vertical lopen
  298. let &winwidth=(&columns/2)
  299. elseif l:window_type ==# 'tab'
  300. tab lopen
  301. else
  302. lopen
  303. endif
  304. setlocal modifiable
  305. for i in range(1, line('$'))
  306. " this is the location-list data for the current item
  307. let d = getloclist(0)[i-1]
  308. " atx headers
  309. if match(d.text, "^#") > -1
  310. let l:level = len(matchstr(d.text, '#*', 'g'))-1
  311. let d.text = substitute(d.text, '\v^#*[ ]*', '', '')
  312. let d.text = substitute(d.text, '\v[ ]*#*$', '', '')
  313. " setex headers
  314. else
  315. let l:next_line = getbufline(bufname(d.bufnr), d.lnum+1)
  316. if match(l:next_line, "=") > -1
  317. let l:level = 0
  318. elseif match(l:next_line, "-") > -1
  319. let l:level = 1
  320. endif
  321. endif
  322. call setline(i, repeat(' ', l:level). d.text)
  323. endfor
  324. setlocal nomodified
  325. setlocal nomodifiable
  326. normal! gg
  327. endfunction
  328. " Convert Setex headers in range `line1 .. line2` to Atx.
  329. "
  330. " Return the number of conversions.
  331. "
  332. function! s:SetexToAtx(line1, line2)
  333. let l:originalNumLines = line('$')
  334. execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n\=+$/# \1/'
  335. execute 'silent! ' . a:line1 . ',' . a:line2 . 'substitute/\v(.*\S.*)\n-+$/## \1/'
  336. return l:originalNumLines - line('$')
  337. endfunction
  338. " If `a:1` is 0, decrease the level of all headers in range `line1 .. line2`.
  339. "
  340. " Otherwise, increase the level. `a:1` defaults to `0`.
  341. "
  342. function! s:HeaderDecrease(line1, line2, ...)
  343. if a:0 > 0
  344. let l:increase = a:1
  345. else
  346. let l:increase = 0
  347. endif
  348. if l:increase
  349. let l:forbiddenLevel = 6
  350. let l:replaceLevels = [5, 1]
  351. let l:levelDelta = 1
  352. else
  353. let l:forbiddenLevel = 1
  354. let l:replaceLevels = [2, 6]
  355. let l:levelDelta = -1
  356. endif
  357. for l:line in range(a:line1, a:line2)
  358. if join(getline(l:line, l:line + 1), "\n") =~ s:levelRegexpDict[l:forbiddenLevel]
  359. echomsg 'There is an h' . l:forbiddenLevel . ' at line ' . l:line . '. Aborting.'
  360. return
  361. endif
  362. endfor
  363. let l:numSubstitutions = s:SetexToAtx(a:line1, a:line2)
  364. for l:level in range(replaceLevels[0], replaceLevels[1], -l:levelDelta)
  365. execute 'silent! ' . a:line1 . ',' . (a:line2 - l:numSubstitutions) . 'substitute/' . s:levelRegexpDict[l:level] . '/' . repeat('#', l:level + l:levelDelta) . '/g'
  366. endfor
  367. endfunction
  368. " Format table under cursor.
  369. "
  370. " Depends on Tabularize.
  371. "
  372. function! s:TableFormat()
  373. let l:pos = getpos('.')
  374. normal! {
  375. " Search instead of `normal! j` because of the table at beginning of file edge case.
  376. call search('|')
  377. normal! j
  378. " Remove everything that is not a pipe othewise well formated tables would grow
  379. " because of addition of 2 spaces on the separator line by Tabularize /|.
  380. s/[^|]//g
  381. Tabularize /|
  382. s/ /-/g
  383. call setpos('.', l:pos)
  384. endfunction
  385. " Wrapper to do move commands in visual mode.
  386. "
  387. function! s:VisMove(f)
  388. norm! gv
  389. call function(a:f)()
  390. endfunction
  391. " Map in both normal and visual modes.
  392. "
  393. function! s:MapNormVis(rhs,lhs)
  394. execute 'nn <buffer><silent> ' . a:rhs . ' :call ' . a:lhs . '()<cr>'
  395. execute 'vn <buffer><silent> ' . a:rhs . ' <esc>:call <sid>VisMove(''' . a:lhs . ''')<cr>'
  396. endfunction
  397. " Parameters:
  398. "
  399. " - step +1 for right, -1 for left
  400. "
  401. " TODO: multiple lines.
  402. "
  403. function! s:FindCornerOfSyntax(lnum, col, step)
  404. let l:col = a:col
  405. let l:syn = synIDattr(synID(a:lnum, l:col, 1), 'name')
  406. while synIDattr(synID(a:lnum, l:col, 1), 'name') ==# l:syn
  407. let l:col += a:step
  408. endwhile
  409. return l:col - a:step
  410. endfunction
  411. " Return the next position of the given syntax name,
  412. " inclusive on the given position.
  413. "
  414. " TODO: multiple lines
  415. "
  416. function! s:FindNextSyntax(lnum, col, name)
  417. let l:col = a:col
  418. let l:step = 1
  419. while synIDattr(synID(a:lnum, l:col, 1), 'name') !=# a:name
  420. let l:col += l:step
  421. endwhile
  422. return [a:lnum, l:col]
  423. endfunction
  424. function! s:FindCornersOfSyntax(lnum, col)
  425. return [<sid>FindLeftOfSyntax(a:lnum, a:col), <sid>FindRightOfSyntax(a:lnum, a:col)]
  426. endfunction
  427. function! s:FindRightOfSyntax(lnum, col)
  428. return <sid>FindCornerOfSyntax(a:lnum, a:col, 1)
  429. endfunction
  430. function! s:FindLeftOfSyntax(lnum, col)
  431. return <sid>FindCornerOfSyntax(a:lnum, a:col, -1)
  432. endfunction
  433. " Returns:
  434. "
  435. " - a string with the the URL for the link under the cursor
  436. " - an empty string if the cursor is not on a link
  437. "
  438. " TODO
  439. "
  440. " - multiline support
  441. " - give an error if the separator does is not on a link
  442. "
  443. function! s:Markdown_GetUrlForPosition(lnum, col)
  444. let l:lnum = a:lnum
  445. let l:col = a:col
  446. let l:syn = synIDattr(synID(l:lnum, l:col, 1), 'name')
  447. if l:syn ==# 'mkdInlineURL' || l:syn ==# 'mkdURL' || l:syn ==# 'mkdLinkDefTarget'
  448. " Do nothing.
  449. elseif l:syn ==# 'mkdLink'
  450. let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
  451. let l:syn = 'mkdURL'
  452. elseif l:syn ==# 'mkdDelimiter'
  453. let l:line = getline(l:lnum)
  454. let l:char = l:line[col - 1]
  455. if l:char ==# '<'
  456. let l:col += 1
  457. elseif l:char ==# '>' || l:char ==# ')'
  458. let l:col -= 1
  459. elseif l:char ==# '[' || l:char ==# ']' || l:char ==# '('
  460. let [l:lnum, l:col] = <sid>FindNextSyntax(l:lnum, l:col, 'mkdURL')
  461. else
  462. return ''
  463. endif
  464. else
  465. return ''
  466. endif
  467. let [l:left, l:right] = <sid>FindCornersOfSyntax(l:lnum, l:col)
  468. return getline(l:lnum)[l:left - 1 : l:right - 1]
  469. endfunction
  470. " Front end for GetUrlForPosition.
  471. "
  472. function! s:OpenUrlUnderCursor()
  473. let l:url = s:Markdown_GetUrlForPosition(line('.'), col('.'))
  474. if l:url != ''
  475. call s:VersionAwareNetrwBrowseX(l:url)
  476. else
  477. echomsg 'The cursor is not on a link.'
  478. endif
  479. endfunction
  480. function! s:VersionAwareNetrwBrowseX(url)
  481. if has('patch-7.4.567')
  482. call netrw#BrowseX(a:url, 0)
  483. else
  484. call netrw#NetrwBrowseX(a:url, 0)
  485. endif
  486. endf
  487. function! s:MapNotHasmapto(lhs, rhs)
  488. if !hasmapto('<Plug>' . a:rhs)
  489. execute 'nmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
  490. execute 'vmap <buffer>' . a:lhs . ' <Plug>' . a:rhs
  491. endif
  492. endfunction
  493. call <sid>MapNormVis('<Plug>Markdown_MoveToNextHeader', '<sid>MoveToNextHeader')
  494. call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousHeader', '<sid>MoveToPreviousHeader')
  495. call <sid>MapNormVis('<Plug>Markdown_MoveToNextSiblingHeader', '<sid>MoveToNextSiblingHeader')
  496. call <sid>MapNormVis('<Plug>Markdown_MoveToPreviousSiblingHeader', '<sid>MoveToPreviousSiblingHeader')
  497. call <sid>MapNormVis('<Plug>Markdown_MoveToParentHeader', '<sid>MoveToParentHeader')
  498. call <sid>MapNormVis('<Plug>Markdown_MoveToCurHeader', '<sid>MoveToCurHeader')
  499. nnoremap <Plug>Markdown_OpenUrlUnderCursor :call <sid>OpenUrlUnderCursor()<cr>
  500. if !get(g:, 'vim_markdown_no_default_key_mappings', 0)
  501. call <sid>MapNotHasmapto(']]', 'Markdown_MoveToNextHeader')
  502. call <sid>MapNotHasmapto('[[', 'Markdown_MoveToPreviousHeader')
  503. call <sid>MapNotHasmapto('][', 'Markdown_MoveToNextSiblingHeader')
  504. call <sid>MapNotHasmapto('[]', 'Markdown_MoveToPreviousSiblingHeader')
  505. call <sid>MapNotHasmapto(']u', 'Markdown_MoveToParentHeader')
  506. call <sid>MapNotHasmapto(']c', 'Markdown_MoveToCurHeader')
  507. call <sid>MapNotHasmapto('gx', 'Markdown_OpenUrlUnderCursor')
  508. endif
  509. command! -buffer -range=% HeaderDecrease call s:HeaderDecrease(<line1>, <line2>)
  510. command! -buffer -range=% HeaderIncrease call s:HeaderDecrease(<line1>, <line2>, 1)
  511. command! -buffer -range=% SetexToAtx call s:SetexToAtx(<line1>, <line2>)
  512. command! -buffer TableFormat call s:TableFormat()
  513. command! -buffer Toc call s:Toc()
  514. command! -buffer Toch call s:Toc('horizontal')
  515. command! -buffer Tocv call s:Toc('vertical')
  516. command! -buffer Toct call s:Toc('tab')