🐯

Vimでアウトライン範囲を選択するtextobject

2022/09/29に公開

以下のような「現在の行と、その次以降の行で現在行よりインデントの深い範囲」を選択するtextobjectを作りました。

foldと組み合わせるとアウトライナー的に使用できます。

箇条書きの入れ子構造を意識して作ったのですが、類似した構造で記述される言語(pythonやlispなど)を開発する際にも便利かもしれません。

TextobjectOutline

以下のコードをvimrcなどで実行してください。
関数内のfromtoが選択範囲の行番号を表しています。

function! s:textobject_outline(...) abort
  let from_parent = index(a:000, 'from_parent') >= 0
  let with_blank = index(a:000, 'with_blank') >= 0

  " get current line and indent
  let from = line('.')
  let indent = indent(from)
  if indent < 0
    return
  endif
  let to = from

  " search first parent
  if from_parent && from > 1 && indent > 0
    let lnum = from - 1
    while indent <= indent(lnum) || (with_blank && getline(lnum) =~ '^\s*$')
      let lnum -= 1
    endwhile

    " update current line and indent
    let from = lnum
    call cursor(from, 0)
    let indent = indent(from)
  endif

  " search last child
  let lnum = to + 1
  while indent < indent(lnum) || (with_blank && getline(lnum) =~ '^\s*$')
    let to = lnum
    let lnum += 1
  endwhile

  " exit visual mode
  let m = mode()
  if m ==# 'v' || m ==# 'V' || m == "\<C-v>"
    execute 'normal! ' .. m
  endif

  " select with line-visual mode
  normal! V
  call cursor(to, 0)
  normal! o
endfunction
command! -nargs=* TextobjectOutline call s:textobject_outline(<f-args>)

xnoremap io <Cmd>TextobjectOutline<CR>
xnoremap ao <Cmd>TextobjectOutline from_parent<CR>
xnoremap iO <Cmd>TextobjectOutline with_blank<CR>
xnoremap aO <Cmd>TextobjectOutline from_parent with_blank<CR>
onoremap io <Cmd>TextobjectOutline<CR>
onoremap ao <Cmd>TextobjectOutline from_parent<CR>
onoremap iO <Cmd>TextobjectOutline with_blank<CR>
onoremap aO <Cmd>TextobjectOutline from_parent with_blank<CR>

ここでは基本動作をio(in outline)にマップしていますが、マッピングはお好みで調整してください。
普通に:TextobjectOutlineを実行して選択することもできます。

動作説明

TextobjectOutlineは2種の文字列引数をとり、それらの有無によって2×2=4通りの動作をします。

  • 通常
    • 現在の行を選択
    • 下の行が現在の行よりインデントが深ければ順次選択
    • 空白行はインデントが0と判定されるので選択しない
  • 引数with_blankをつける
    • 空白行も含める
  • 引数with_parentをつける
    • 現在行より上で、現在よりインデントが浅い行(親)を起点とする
    • 現在のインデントが0の場合は無効

参考

vim-textobj-indentが類似の機能を提供していますが、微妙に違いました。

https://github.com/kana/vim-textobj-indent

visual modeを脱出する処理などはmini.nvimのindentscopeモジュールを参考にしました。

https://github.com/echasnovski/mini.nvim

markdownで箇条書きを書きやすくする設定に関してはこちら。

https://zenn.dev/kawarimidoll/articles/4564e6e5c2866d

Discussion