Created
January 29, 2026 13:43
-
-
Save Konfekt/68ff9fb95af48502c119f16a02f09fd3 to your computer and use it in GitHub Desktop.
Drop-in Vim(9) Markdown text objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if !has("vim9script") | finish | endif | |
| vim9script | |
| # Adapted from https://github.com/habamax/.vim/blob/7e2bac788a3a6de59356374ea60d90b5dec68b2e/after/ftplugin/markdown.vim | |
| # Markdown header text object | |
| # * inner object is the text between prev section header(excluded) and the next | |
| # section of the same level(excluded) or end of file. | |
| # * an object is the text between prev section header(included) and the next section of the same | |
| # level(excluded) or end of file. | |
| def HeaderTextObj(inner: bool) | |
| var lnum_start = search('^#\+\s\+[^[:space:]=]', "ncbW") | |
| if lnum_start > 0 | |
| var lvlheader = matchstr(getline(lnum_start), '^#\+') | |
| var lnum_end = search('^#\{1,' .. len(lvlheader) .. '}\s', "nW") | |
| if lnum_end == 0 | |
| lnum_end = search('\%$', 'cnW') | |
| else | |
| lnum_end -= 1 | |
| endif | |
| if inner && getline(lnum_start + 1) !~ '^#\+\s\+[^[:space:]=]' | |
| lnum_start += 1 | |
| endif | |
| exe $":{lnum_end}" | |
| normal! V | |
| exe $":{lnum_start}" | |
| endif | |
| enddef | |
| # Markdown Unit/header textobject | |
| onoremap <buffer><silent> iu <scriptcmd>HeaderTextObj(true)<CR> | |
| onoremap <buffer><silent> au <scriptcmd>HeaderTextObj(false)<CR> | |
| xnoremap <buffer><silent> iu <esc><scriptcmd>HeaderTextObj(true)<CR> | |
| xnoremap <buffer><silent> au <esc><scriptcmd>HeaderTextObj(false)<CR> | |
| def FindSection(dir: string = '') | |
| search('\%(^#\{1,5\}\s\+\S\|^\S.*\n^[=-]\+$\)', $'{dir}sW') | |
| var mdsyn = synstack(line('.'), 1)->map('synIDattr(v:val, "name")') | |
| while mdsyn[0] =~ '\v^mkdCode%(Block)?|pandoc%(DelimitedCodeBlock|NoFormatted)' | |
| search('\%(^#\{1,5\}\s\+\S\|^\S.*\n^[=-]\+$\)', $'{dir}sW') | |
| mdsyn = synstack(line('.'), 1)->map('synIDattr(v:val, "name")') | |
| endwhile | |
| normal! zz | |
| enddef | |
| nnoremap <buffer> ]] <scriptcmd>FindSection()<CR> | |
| nnoremap <buffer> [[ <scriptcmd>FindSection('b')<CR> | |
| # code block text object | |
| def ObjCode(inner: bool) | |
| cursor(line('.'), 1) | |
| def IsCode(): bool | |
| var stx = map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")')->join() | |
| return stx =~? 'mkdCode\|pandocDelimitedCodeBlock' | |
| enddef | |
| def IsCodeDelimiter(): bool | |
| var stx = map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")')->join() | |
| return stx =~? '\vmkdCodeDelimiter|pandocDelimitedCodeBlock%(Start|End)' | |
| enddef | |
| if exists("g:syntax_on") && index(['markdown', 'pandoc'], &syntax) >= 0 && | |
| \ !IsCode() && !IsCodeDelimiter() | |
| if search('^\s*```', 'cW', line(".") + 500, 100) <= 0 | |
| return | |
| endif | |
| elseif !IsCodeDelimiter() || (!IsCode() && IsCodeDelimiter()) | |
| if search('^\s*```', 'bW') <= 0 | |
| return | |
| endif | |
| endif | |
| var pos_start = line('.') + (inner ? 1 : 0) | |
| # Search for the code end. | |
| if search('^\s*```\s*$', 'W') <= 0 | return | endif | |
| var pos_end = line('.') - (inner ? 1 : 0) | |
| exe $":{pos_start}" | |
| normal! V | |
| exe $":{pos_end}" | |
| enddef | |
| onoremap <buffer> <silent>il <scriptcmd>ObjCode(true)<CR> | |
| onoremap <buffer> <silent>al <scriptcmd>ObjCode(false)<CR> | |
| xnoremap <buffer> <silent>il <esc><scriptcmd>ObjCode(true)<CR> | |
| xnoremap <buffer> <silent>al <esc><scriptcmd>ObjCode(false)<CR> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment