【Neovim】lualineの表示をカスタムしてみる。Visualモードで選択された行数、文字数を表示してみる
はじめに
Neovimのステータスラインの表示に、lualineを愛用しています。
branch
でgit branchの表示や、filetype
など、表示可能なコンポーネントも便利です。
提供されているもののうち、selectioncount
の挙動が自分には合わなかったので、カスタムコンポーネントを書いて指定してみます。
環境
- Nvim
- Plugin:lualine
lualineについて
lualine.nvimはluaで設定可能なステータスラインのプラグインです。
ドキュメントにサンプルとして以下のようなものがありますが、華やか&軽快に表示可能です。
インストール方法
に書いてあります。
私はプラグイン管理に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
以下のような不満がありました。
- 複数行にまたがったときには行数の表示のみになる
- 日本語などのマルチバイト文字列が正確にカウントできない <-- 重要
日本語はいつだってわれわれの前にたちふさがる...つらい...
自作関数を指定してみる
なのでこんな関数を作成し、指定することで解決です。
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