📝

【Neovim】lualineの表示をカスタムしてみる。Visualモードで選択された行数、文字数を表示してみる

2023/11/19に公開

はじめに

Neovimのステータスラインの表示に、lualineを愛用しています。
https://github.com/nvim-lualine/lualine.nvim/tree/master

branchでgit branchの表示や、filetypeなど、表示可能なコンポーネントも便利です。
提供されているもののうち、selectioncountの挙動が自分には合わなかったので、カスタムコンポーネントを書いて指定してみます。

環境

  • Nvim
  • Plugin:lualine

lualineについて

https://github.com/nvim-lualine/lualine.nvim/tree/master
lualine.nvimはluaで設定可能なステータスラインのプラグインです。

ドキュメントにサンプルとして以下のようなものがありますが、華やか&軽快に表示可能です。

インストール方法

https://github.com/nvim-lualine/lualine.nvim/tree/master#installation
に書いてあります。

私はプラグイン管理にlazy.nvimを使っているため、以下のようにしてます。
(設定はlualine.luaとして別ファイルに出してます)

  {
    'nvim-lualine/lualine.nvim',
    dependencies = {
      'nvim-tree/nvim-web-devicons',
    },
    config = function()
      require('plugins.config.lualine')
    end,
  },

設定

デフォルトは以下のような感じです。

require('lualine').setup {
  options = {
    icons_enabled = true,
    theme = 'auto',
    component_separators = { left = '', right = ''},
    section_separators = { left = '', right = ''},
    disabled_filetypes = {
      statusline = {},
      winbar = {},
    },
    ignore_focus = {},
    always_divide_middle = true,
    globalstatus = false,
    refresh = {
      statusline = 1000,
      tabline = 1000,
      winbar = 1000,
    }
  },
  sections = {
    lualine_a = {'mode'},
    lualine_b = {'branch', 'diff', 'diagnostics'},
    lualine_c = {'filename'},
    lualine_x = {'encoding', 'fileformat', 'filetype'},
    lualine_y = {'progress'},
    lualine_z = {'location'}
  },
  inactive_sections = {
    lualine_a = {},
    lualine_b = {},
    lualine_c = {'filename'},
    lualine_x = {'location'},
    lualine_y = {},
    lualine_z = {}
  },
  tabline = {},
  winbar = {},
  inactive_winbar = {},
  extensions = {}
}

a,b,c,x,y,zと書かれているところで表示場所がわかれていて、以下のような配置になります。

+-------------------------------------------------+
| A | B | C                             X | Y | Z |
+-------------------------------------------------+

上記の設定に書かれている、'branch'などが標準のコンポーネント名の指定です。
執筆時点では、以下のようなものが提供されています。

- branch (git branch)
- buffers (shows currently available buffers)
- diagnostics (diagnostics count from your preferred source)
- diff (git diff status)
- encoding (file encoding)
- fileformat (file format)
- filename
- filesize
- filetype
- hostname
- location (location in file in line:column format)
- mode (vim mode)
- progress (%progress in file)
- searchcount (number of search matches when hlsearch is active)
- selectioncount (number of selected characters or lines)
- tabs (shows currently available tabs)
- windows (shows currently available windows)

また、上記の文字列以外に自作関数も指定できるとのことです。

local function hello()
  return [[hello world]]
end
sections = { lualine_a = { hello } }

本題:文字数カウントを表示したい

提供されているコンポーネントとして、selectioncountがあります。
実装としては以下のようになっているため、

local function selectioncount()
  local mode = vim.fn.mode(true)
  local line_start, col_start = vim.fn.line('v'), vim.fn.col('v')
  local line_end, col_end = vim.fn.line('.'), vim.fn.col('.')
  if mode:match('') then
    return string.format('%dx%d', math.abs(line_start - line_end) + 1, math.abs(col_start - col_end) + 1)
  elseif mode:match('V') or line_start ~= line_end then
    return math.abs(line_start - line_end) + 1
  elseif mode:match('v') then
    return math.abs(col_start - col_end) + 1
  else
    return ''
  end
end

return selectioncount

https://github.com/nvim-lualine/lualine.nvim/blob/2248ef254d0a1488a72041cfb45ca9caada6d994/lua/lualine/components/selectioncount.lua

以下のような不満がありました。

  • 複数行にまたがったときには行数の表示のみになる
  • 日本語などのマルチバイト文字列が正確にカウントできない <-- 重要

日本語はいつだってわれわれの前にたちふさがる...つらい...

自作関数を指定してみる

なのでこんな関数を作成し、指定することで解決です。

local function selectionCount()
    local mode = vim.fn.mode()
    local start_line, end_line, start_pos, end_pos

    -- 選択モードでない場合には無効
    if not (mode:find("[vV\22]") ~= nil) then return "" end
    start_line = vim.fn.line("v")
    end_line = vim.fn.line(".")

    if mode == 'V' then
        -- 行選択モードの場合は、各行全体をカウントする
        start_pos = 1
        end_pos = vim.fn.strlen(vim.fn.getline(end_line)) + 1
    else
        start_pos = vim.fn.col("v")
        end_pos = vim.fn.col(".")
    end

    local chars = 0
    for i = start_line, end_line do
        local line = vim.fn.getline(i)
        local line_len = vim.fn.strlen(line)
        local s_pos = (i == start_line) and start_pos or 1
        local e_pos = (i == end_line) and end_pos or line_len + 1
        chars = chars + vim.fn.strchars(line:sub(s_pos, e_pos - 1))
    end

    local lines = math.abs(end_line - start_line) + 1
    return tostring(lines) .. " lines, " .. tostring(chars) .. " characters"
end

ポイントはNeovimのLua APIのvim.fn.strcharsを使用して文字数のカウントをしていることです。これにより、マルチバイト文字列の境界が正確に認識されなかったり、文字数のカウントを考慮したりする必要がなくなります。

※試行錯誤途中で放り出しましたが、上記を使用しないと chars = chars + vim.fn.strchars(line:sub(s_pos, e_pos - 1))の部分を以下のような雰囲気の冗長な処理をする必要があります。。

 -- マルチバイト文字の末尾を考慮する
        if end_col ~= -1 and e_col < line_len then
            while e_col < line_len and vim.fn.strlen(vim.fn.strcharpart(line, e_col, 1)) > 1 do
                e_col = e_col + 1
            end
        end

        -- マルチバイト文字の先頭を考慮する
        if i > start_line then
            while s_col > 0 and vim.fn.strlen(vim.fn.strcharpart(line, s_col - 1, 1)) > 1 do
                s_col = s_col - 1
            end
        end

        chars = chars + (vim.str_utfindex(line, e_col) - vim.str_utfindex(line, s_col))
    end

まとめ

簡単なlualineの説明と、カスタムコンポーネントの指定をして文字数カウントをしてみました。
学びとして、日本語の扱いでマルチバイト文字列の文字境界の認識や文字数のカウントはなにも考えないでやるとハマりどころですね。。

参考までに、私のlualineの設定ファイルを以下に貼っておきます。
tab番号表示、mode、branch、ファイル名、visualモードでの選択文字数表示、LSPのエラーなど、ファイル形式、拡張子、行数などを表示してます。

local function selectionCount()
    local mode = vim.fn.mode()
    local start_line, end_line, start_pos, end_pos

    -- 選択モードでない場合には無効
    if not (mode:find("[vV\22]") ~= nil) then return "" end
    start_line = vim.fn.line("v")
    end_line = vim.fn.line(".")

    if mode == 'V' then
        -- 行選択モードの場合は、各行全体をカウントする
        start_pos = 1
        end_pos = vim.fn.strlen(vim.fn.getline(end_line)) + 1
    else
        start_pos = vim.fn.col("v")
        end_pos = vim.fn.col(".")
    end

    local chars = 0
    for i = start_line, end_line do
        local line = vim.fn.getline(i)
        local line_len = vim.fn.strlen(line)
        local s_pos = (i == start_line) and start_pos or 1
        local e_pos = (i == end_line) and end_pos or line_len + 1
        chars = chars + vim.fn.strchars(line:sub(s_pos, e_pos - 1))
    end

    local lines = math.abs(end_line - start_line) + 1
    return tostring(lines) .. " lines, " .. tostring(chars) .. " characters"
end
require('lualine').setup {
  options = {
    icons_enabled = true,
    theme = 'auto',
    component_separators = { left = '|', right = '|' },
    section_separators = { left = '', right = '' },
    disabled_filetypes = {},
    always_divide_middle = true,
    colored = false,
  },
  sections = {
    lualine_a = { 'mode' },
    lualine_b = { 'branch', 'diff' },
    lualine_c = {
      {
        'filename',
        path = 1,
        file_status = true,
        shorting_target = 40,
        symbols = {
          modified = ' [+]',
          readonly = ' [RO]',
          unnamed = 'Untitled',
        }
      }
    },
    lualine_x = {
      {'searchcount'},
      {selectionCount},
      {
        'diagnostics',
        sources = {
          -- 'nvim_diagnostic', 
          'nvim_lsp',
        },

        sections = { 'error', 'warn', 'info', 'hint' },

        diagnostics_color = {
          error = 'DiagnosticError',
          warn  = 'DiagnosticWarn',
          info  = 'DiagnosticInfo',
          hint  = 'DiagnosticHint',
        },
        symbols = { error = 'E', warn = 'W', info = 'I', hint = 'H' },
        colored = true,
        update_in_insert = false,
        always_visible = false,
      },
    },
    lualine_y = { 'filetype', 'encoding' },
    lualine_z = {
      'location',
      'progress'
    }
  },
  inactive_sections = {
    lualine_a = {},
    lualine_b = {},
    lualine_c = { 'filename' },
    lualine_x = { 'location' },
    lualine_y = {},
    lualine_z = {}
  },
  tabline = {
    lualine_a = {
      {
        'buffers',
        mode = 4,
        icons_enabled = true,
        show_filename_only = true,
        hide_filename_extensions = false
      }
    },
    lualine_b = {},
    lualine_c = {},
    lualine_x = {},
    lualine_y = {},
    lualine_z = { 'tabs' }
  },
  extensions = {}
}

Discussion