diff --git a/README.md b/README.md
index fd1a94d..50cb5cf 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ for `lualine.nvim` with additional components.
- [ex.spellcheck](#exspellcheck)
- [ex.cwd](#excwd)
- [ex.location](#exlocation)
+ - [ex.progress](#exprogress)
- [ex.relative_filename](#exrelative_filename)
- [ex.git.branch](#exgitbranch)
- [ex.lsp.single](#exlspsingle)
@@ -167,6 +168,42 @@ sections = {
}
```
+### ex.progress
+
+This component shows the progress in the file. It has two pre-build modes: 'percent' and 'bar'. The first
+one is similar to the default `progress` component, but in the second 'bar' mode the progress is
+shown as a progress bar.
+
+| mode | example |
+|:---:|:---:|
+| `'percent'` | |
+| `'bar'` | |
+
+```lua
+sections = {
+ lualine_a = {
+ {
+ 'ex.progress',
+
+ -- How to show the progress. It may be the one of two string constants:
+ -- 'percent' or 'bar'. In the 'percent' mode the progress is shown as percent of the file.
+ -- In the 'bar' mode it's shown as the vertical bar. Also, it can be a table with symbols
+ -- which will be taken to show according to the progress, or a function, which receive three
+ -- arguments: the component itself, the cursor line and total lines count in the file.
+ mode = 'percent',
+
+ -- This string will be shown when the cursor is on the first line of the file. Set `false`
+ -- to turn this logic off.
+ top = 'Top',
+
+ -- This string will be shown when the cursor is on the last line of the file. Set `false`
+ -- to turn this logic off.
+ bottom = 'Bot'
+ }
+ }
+}
+```
+
### ex.relative_filename
This component shows a `filename`: a file name and a path to the current file relative to the
diff --git a/lua/lualine/components/ex/progress.lua b/lua/lualine/components/ex/progress.lua
new file mode 100644
index 0000000..9e34787
--- /dev/null
+++ b/lua/lualine/components/ex/progress.lua
@@ -0,0 +1,48 @@
+-- stylua: ignore start
+local progress_bar = { '█', '▇', '▆', '▅', '▄', '▃', '▂', '▁', ' ' }
+-- stylua: ignore end
+
+local Progress = require('lualine.ex.component'):extend({
+ mode = 'percent',
+ top = 'Top',
+ bottom = 'Bot',
+})
+
+local function mode_percent(self, line, total)
+ if (line == total) and self.options.bottom then
+ return self.options.bottom
+ end
+ if (line == 1) and self.options.top then
+ return self.options.top
+ end
+ return string.format('%3d%%%%', 99 * line / total + 1)
+end
+
+local function mode_table(t)
+ return function(self, line, total)
+ if (line == total) and self.options.bottom then
+ return self.options.bottom
+ end
+ if (line == 1) and self.options.top then
+ return self.options.top
+ end
+ local idx = math.floor(line * (#t - 1) / total) + 1
+ return t[idx]
+ end
+end
+
+function Progress:post_init()
+ if type(self.options.mode) == 'string' then
+ self.options.mode = (self.options.mode == 'bar') and mode_table(progress_bar)
+ or mode_percent
+ end
+ if type(self.options.mode) == 'table' then
+ self.options.mode = mode_table(self.options.mode)
+ end
+end
+
+function Progress:update_status()
+ return self.options.mode(self, vim.fn.line('.'), vim.fn.line('$'))
+end
+
+return Progress
diff --git a/tests/components/progress_spec.lua b/tests/components/progress_spec.lua
new file mode 100644
index 0000000..3c238fe
--- /dev/null
+++ b/tests/components/progress_spec.lua
@@ -0,0 +1,92 @@
+local l = require('tests.ex.lualine')
+local t = require('tests.ex.busted') --:ignore_all_tests()
+
+local eq = assert.are.equal
+
+local orig = {
+ line = vim.fn.line,
+}
+local mock = {
+ line = {},
+}
+
+local component_name = 'ex.progress'
+
+describe(component_name, function()
+ before_each(function()
+ vim.fn.line = function(arg)
+ return mock.line[arg]
+ end
+ end)
+
+ after_each(function()
+ vim.fn.line = orig.line
+ end)
+
+ describe('in "percent" (default) mode', function()
+ it('should show "Top" when the cursor is on bottom of the buffer', function()
+ -- { line, total, expected percent }
+ local cases = {
+ { 1, 100, 'Top' },
+ { 1, 1000, 'Top' },
+ { 2, 1000, ' 1%%' },
+ }
+ for _, case in ipairs(cases) do
+ mock.line['.'] = case[1]
+ mock.line['$'] = case[2]
+ local c = l.render_component(component_name)
+ eq(case[3], c, vim.inspect(case))
+ end
+ end)
+ it('should show "Bot" when the cursor is on bottom of the buffer', function()
+ -- { line, total, expected percent }
+ local cases = {
+ { 100, 100, 'Bot' },
+ { 1000, 1000, 'Bot' },
+ { 999, 1000, ' 99%%' },
+ }
+ for _, case in ipairs(cases) do
+ mock.line['.'] = case[1]
+ mock.line['$'] = case[2]
+ local c = l.render_component(component_name)
+ eq(case[3], c, vim.inspect(case))
+ end
+ end)
+ it('should show expected persentage of the buffer', function()
+ -- { line, total, expected percent }
+ local cases = {
+ { 1, 100, 1 },
+ { 100, 100, 100 },
+ { 17, 111, 16 },
+ }
+ for _, case in ipairs(cases) do
+ mock.line['.'] = case[1]
+ mock.line['$'] = case[2]
+ local c = l.render_component(component_name, { top = false, bottom = false })
+ eq(string.format('%3d%%%%', case[3]), c, vim.inspect(case))
+ end
+ end)
+ end)
+
+ describe('in "bar" mode, or "tabe" mode', function()
+ it('should show appropriate symbol', function()
+ -- { line, total, expected percent }
+ local cases = {
+ { 1, 100, '█' },
+ { 100, 100, ' ' },
+ { 99, 100, '▁' },
+ { 7, 9, '▂' },
+ { 19, 31, '▄' },
+ }
+ for _, case in ipairs(cases) do
+ mock.line['.'] = case[1]
+ mock.line['$'] = case[2]
+ local c = l.render_component(
+ component_name,
+ { top = false, bottom = false, mode = 'bar' }
+ )
+ eq(case[3], c, vim.inspect(case))
+ end
+ end)
+ end)
+end)