hasciiの日記

多分プログラミング関係

Vim で現在カーソルがある行と同じインデントを強調表示

最近ちょっと Vimpython を読む機会があったのだけど、ぱっと見てインデントが同じ行がどこまで分からず読み辛かった。
そんなわけで、現在カーソルがある行と同じ(もしくはそれ以上の)インデントレベルのところを強調表示する方法を模索したのでそのメモ

方法1: vim-indent-guides を使う

インデントを強調表示する、で検索すると出てきたのがこれ。

パッと使った感じ、以下の機能があった

  • 等間隔で列を強調表示可能
  • 色は自由に指定可能

多分もっと高機能っぽいけど、今回はこんなにおおげさな機能はいらなかった(というかデフォルトのままだと情報量が多すぎる。カスタマイズするのは面倒だった)のでパス

方法2: 適宜 highlight パターンを作る

今回とったのはこっち。
現在カーソルがある行の文字列を取得して、highlight 対象とするパターンを返却する簡単な関数を作った。

以下その関数

function! s:indent_pattern()
  let l:line = getline('.')
  let l:indent = matchstr(l:line, '^\zs\s\+\ze\S')
  let l:len = len(l:indent) - 1
  if l:len > 0
    return '^\s\{'. l:len . '}\zs\s\ze'
  else
    return '^\zs\s\ze'
  endif
endfunction

# これだとスペースとタブが入り混じってると変な highlight になるけど気にしない。そんなコード書く方が悪い。

この関数から取得したパターンを matchadd で適当な highlight に追加してやればやりたいことができるはず。
matchadd とか matchdel する仕組みについては、ここ何回か同じような仕組みでほげほげしてきたので、ある程度汎用的に使えるようにした。

以下そのコード

" defined highlight sources
let g:auto_highlights = {}

" control enable/disable auto highlighting
let g:enable_auto_highlight = 1

" disable auto highlight when filetype in below
let g:auto_highlight_disable_file_type = ['unite', 'tmp']

function! s:compare_source(i1, i2)
  return a:i1.priority == a:i2.priority ? 0 : a:i1.priority > a:i2.priority ? 1 : -1
endfunction

function! s:init_window_auto_highlight()
  let l:info = {}
  for highlight_kind in keys(g:auto_highlights)
    let l:info[highlight_kind] ={
          \ 'current_match_pattern' : '',
          \ 'current_match_id' : -1,
          \}
  endfor
  return l:info
endfunction

" function for add highlight source
function! s:define_auto_highlight_source(src)
  if !has_key(g:auto_highlights, a:src.highlight)
    let g:auto_highlights[a:src.highlight] = {
          \ 'sources' : [],
          \}
  endif
  call add(g:auto_highlights[a:src.highlight].sources, a:src)
  call sort(g:auto_highlights[a:src.highlight].sources, 's:compare_source')
endfunction

" main
function! s:start_highlight()
  if !get(g:, "enable_auto_highlight", 0) || index(get(g:, 'auto_highlight_disable_file_type', []), &filetype) >= 0
    return
  endif
  if !exists('w:auto_highlight_info')
    let w:auto_highlight_info = s:init_window_auto_highlight()
  endif
  for [item, value] in items(g:auto_highlights)
    for src in value.sources
      let l:pattern = src.pattern()
      if !empty(l:pattern)
        let l:target = w:auto_highlight_info[item]
        if l:target['current_match_pattern'] != l:pattern
          call s:ClearHighlight(item)
          let l:target['current_match_pattern'] = l:pattern
          let l:target['current_match_id'] = matchadd(item, l:pattern, src.priority)
        endif
        break
      endif
    endfor
  endfor
endfunction

" clear before highlight
function! s:ClearHighlight(kind)
  if exists('w:auto_highlight_info')
    if w:auto_highlight_info[a:kind].current_match_id >= 0
      call matchdelete(w:auto_highlight_info[a:kind].current_match_id)
      let w:auto_highlight_info[a:kind].current_match_id = -1
      let w:auto_highlight_info[a:kind].current_match_pattern = ''
    endif
  endif
endfunction

" toggle enable/disable auto highlight
function! s:toggle_auto_highlight()
  let g:enable_auto_highlight = !get(g:, "enable_auto_highlight", 0)
  call s:CheckEnableHighlightCurrentWord()
endfunction

function! s:CheckEnableHighlightCurrentWord()
  if !g:enable_auto_highlight
    for kind in keys(g:auto_highlights)
      call s:ClearHighlight(kind)
  endfor
  endif
endfunction

command! -bar ToggleCurrentHighlight call s:toggle_auto_highlight()
command! -bar CurrentHighlight call s:start_highlight()

後、適当な autocmd に CurrentHighlight を実行するように設定してやる。
で、これまで作成した highlight を登録

" current word highlight
let s:cword_highlight = {
      \ 'name': 'current_word',
      \ 'highlight': 'CurrentWord',
      \ 'priority': 10,
      \}

function! s:EscapeText( text )
  return substitute( escape(a:text, '\' . '^$.*[~'), "\n", '\\n', 'ge' )
endfunction

function! s:cword_highlight.pattern()
  let l:cwd = expand('<cword>')
  if empty(l:cwd)
    return ''
  else
    let l:regexp = s:EscapeText(l:cwd)
    return l:cwd =~# '^\k\+$' ? '\<' . l:regexp . '\>' : l:regexp
  endif
endfunction
call s:define_auto_highlight_source(s:cword_highlight)

" matchit highlight
let s:matchit_highlight = {
      \ 'name': 'matchit',
      \ 'highlight': 'CurrentWord', 
      \ 'priority': 9,
      \}

function! s:InitMatchit()
  if !exists('b:match_words')
    return
  endif
  let l:mw = filter(split(b:match_words, ',\|:'), 'v:val !~ "^[(){}[\\]]$"')
  let b:reserved_regexp = join(l:mw, '\|')
  let mwre = '\%(' . b:reserved_regexp . '\)'
  let b:mwre = substitute(mwre, "'", "''", 'g')
endfunction

function! s:matchit_highlight.pattern()
  if !exists('b:match_words')
    return ''
  endif
  if !exists('b:reserved_regexp')
    call s:InitMatchit()
  endif
  if expand("<cword>") !~ b:reserved_regexp || empty(b:reserved_regexp)
    return ''
  endif
  let lcs = []
  let wsv = winsaveview()
  while 1
    exe 'normal %'
    let lc = {'line': line('.'), 'col': col('.')}
    if len(lcs) > 0 && (lcs[0] == lc || lcs[-1] == lc)
      break
    endif
    call add(lcs, lc)
  endwhile
  call winrestview(wsv)

  call map(lcs, '"\\%" . v:val.line . "l\\%" . v:val.col . "c"')
  let lcre = join(lcs, '\|')
  " final \& part of the regexp is a hack to improve html
  return '.*\%(' . lcre . '\).*\&' . b:mwre
  "return '.*\%(' . lcre . '\).*\&' . b:mwre . '\&\% (<\_[^>]\+>\|.*\)'
endfunction
call s:define_auto_highlight_source(s:matchit_highlight)

" indent highlight
let s:indent_highlight = {
      \ 'name': 'indent',
      \ 'highlight': 'Indent', 
      \ 'priority': 10,
      \}

function! s:indent_highlight.pattern()
  let l:line = getline('.')
  let l:indent = matchstr(l:line, '^\zs\s\+\ze\S')
  let l:len = len(l:indent) - 1
  if l:len > 0
    return '^\s\{'. l:len . '}\zs\s\ze'
  else
    return '^\zs\s\ze'
  endif
endfunction
call s:define_auto_highlight_source(s:indent_highlight)

作っておいて何だけど多分これ以上 source 増やさない。あんまり情報量増えても害なので…

以下あったら嬉しいかもしれない機能

  • 変数の上にカーソルがあるとき、その変数の定義部分 or 代入部分だけ色を変える