Repository: nvim-lualine/lualine.nvim
Pull request #1368 (2025-01-17 01:20:01)

fix: insert r_padding within final highlight
When a component ends with a custom highlight, any `r_padding` should be added inside that highlight, not after, just like how `l_padding` is added inside a custom starting highlight. This PR takes care of that.

This is required to fully address https://github.com/folke/trouble.nvim/issues/569 (see also https://github.com/folke/trouble.nvim/pull/616 for the fix to trouble.nvim's own part in the issue).

**Before:**
![Screen Shot 2025-01-16 at 16 53 11 PM](https://github.com/user-attachments/assets/c576c962-f3d2-4500-8def-123228b79391)

**After:**
![Screen Shot 2025-01-16 at 17 18 20 PM](https://github.com/user-attachments/assets/8f8179d1-a444-428d-8b30-34bba96dec45)


Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..8e9dd76
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,1 @@
+custom: ["https://buymeacoffee.com/shadmansalj"]
diff --git a/.github/workflows/autogen.yml b/.github/workflows/autogen.yml
index bde7d91..26db6aa 100644
--- a/.github/workflows/autogen.yml
+++ b/.github/workflows/autogen.yml
@@ -5,6 +5,7 @@ on:
     paths:
       - "lua/**.lua"
       - "examples/**.lua"
+      - "tests/**.lua"
       - ".stylua.toml"
       - "README.md"
 
@@ -16,7 +17,7 @@ concurrency:
 jobs:
   autogen:
     name: "(vimdoc|formating)"
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     timeout-minutes: 10
     permissions:
       contents: write
@@ -32,7 +33,7 @@ jobs:
         uses: JohnnyMorganz/stylua-action@1.0.0
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
-          args: --config-path=.stylua.toml lua/ examples/
+          args: --config-path=.stylua.toml lua/ examples/ tests/
           version: 0.14.1
       - name: Push changes
         uses: stefanzweifel/git-auto-commit-action@v4
@@ -41,4 +42,4 @@ jobs:
           commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com
           commit_message: "chore: autogen (vimdocs+formating)"
           branch: ${{ github.head_ref }}
-          file_pattern: lua/ examples/ doc/lualine.txt
+          file_pattern: lua/ examples/ tests/ doc/lualine.txt
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b9d52e..4a57517 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,6 @@ concurrency:
   group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
   cancel-in-progress: true
 
-
 jobs:
   tests:
     name: ${{ matrix.os }} ${{ matrix.flavor }}
@@ -20,19 +19,19 @@ jobs:
       matrix:
         include:
           - flavor: nvim-0.7
-            runner: ubuntu-20.04
+            runner: ubuntu-22.04
             os: linux
             nvim_version: v0.7.0
           - flavor: nvim-0.8
-            runner: ubuntu-20.04
+            runner: ubuntu-22.04
             os: linux
             nvim_version: v0.8.0
           - flavor: nvim-0.9
-            runner: ubuntu-20.04
+            runner: ubuntu-22.04
             os: linux
             nvim_version: v0.9.0
           - flavor: nvim-nightly
-            runner: ubuntu-20.04
+            runner: ubuntu-22.04
             os: linux
             nvim_version: nightly
     runs-on: ${{ matrix.runner }}
@@ -48,13 +47,13 @@ jobs:
       - name: Setup neovim ${{matrix.nvim_version}}
         uses: rhysd/action-setup-vim@v1
         with:
-            neovim: true
-            version: ${{matrix.nvim_version}}
+          neovim: true
+          version: ${{matrix.nvim_version}}
       - name: Run tests
         run: |
           make test
   lint:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
diff --git a/Makefile b/Makefile
index 131d022..d9c30bf 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ lint:
 	@luacheck examples/
 
 format:
-	@stylua --config-path=.stylua.toml lua/ examples/
+	@stylua --config-path=.stylua.toml lua/ examples/ tests/
 
 test:
 	@mkdir -p tmp_home
diff --git a/README.md b/README.md
index 4c03998..cba0e52 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
 
 ![code size](https://img.shields.io/github/languages/code-size/nvim-lualine/lualine.nvim?style=flat-square)
 ![license](https://img.shields.io/github/license/nvim-lualine/lualine.nvim?style=flat-square)
+[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-F7941D?style=plastic&logo=buy-me-a-coffee&logoColor=white)](https://buymeacoffee.com/shadmansalj)
 
 <!-- panvimdoc-ignore-end -->
 
@@ -268,6 +269,7 @@ sections = {lualine_a = {'mode'}}
 - `selectioncount` (number of selected characters or lines)
 - `tabs` (shows currently available tabs)
 - `windows` (shows currently available windows)
+- `lsp_status` (shows active LSPs in the current buffer and a progress spinner)
 
 #### Custom components
 
@@ -792,6 +794,29 @@ sections = {
 }
 ```
 
+#### lsp status component options
+
+```lua
+sections = {
+  lualine_a = {
+    {
+      'lsp_status',
+      icon = '', -- f013
+      symbols = {
+        -- Standard unicode symbols to cycle through for LSP progress:
+        spinner = { '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' },
+        -- Standard unicode symbol for when LSP is done:
+        done = '✓',
+        -- Delimiter inserted between LSP names:
+        separator = ' ',
+      },
+      -- List of LSP names to ignore (e.g., `null-ls`):
+      ignore_lsp = {},
+    }
+  }
+}
+```
+
 ---
 
 ### Tabline
@@ -1037,3 +1062,11 @@ You can find some useful [configuration snippets](https://github.com/nvim-lualin
 
 If you want to extend lualine with plugins or want to know
 which ones already do, [wiki/plugins](https://github.com/nvim-lualine/lualine.nvim/wiki/Plugins) is for you.
+
+### Support
+
+If you find this project useful, consider supporting development:
+
+<a href="https://buymeacoffee.com/shadmansalj" target="_blank">
+    <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" width="200">
+</a>
diff --git a/THEMES.md b/THEMES.md
index 379b620..c82b524 100644
--- a/THEMES.md
+++ b/THEMES.md
@@ -54,7 +54,7 @@ But if `g:ayuprefermirage` exists, it will load ayu_mirage instead when
 
 ### base16
 
-This theme will automatically use colors defined by your colorscheme using [tinted-theming/base16-vim](https://github.com/tinted-theming/base16-vim) or [RRethy/nvim-base16](https://github.com/RRethy/nvim-base16)] plugin.
+This theme will automatically use colors defined by your colorscheme using [tinted-theming/tinted-vim](https://github.com/tinted-theming/tinted-vim) or [RRethy/nvim-base16](https://github.com/RRethy/nvim-base16)] plugin.
 The following example is using the `tomorrow-night` colorscheme:
 
 <p>
diff --git a/doc/lualine.txt b/doc/lualine.txt
index ccc6ebe..c9ad7b8 100644
--- a/doc/lualine.txt
+++ b/doc/lualine.txt
@@ -268,6 +268,7 @@ CHANGING COMPONENTS IN LUALINE SECTIONS ~
 - `selectioncount` (number of selected characters or lines)
 - `tabs` (shows currently available tabs)
 - `windows` (shows currently available windows)
+- `lsp_status` (shows active LSPs in the current buffer and a progress spinner)
 
 
                                                    *lualine-Custom-components*
@@ -825,6 +826,30 @@ Component specific options             These are options that are available on
 <
 
 
+                                        *lualine-lsp-status-component-options*
+
+>
+    sections = {
+      lualine_a = {
+        {
+          'lsp_status',
+          icon = '', -- f013
+          symbols = {
+            -- Standard unicode symbols to cycle through for LSP progress:
+            spinner = { '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' },
+            -- Standard unicode symbol for when LSP is done:
+            done = '✓',
+            -- Delimiter inserted between LSP names:
+            separator = ' ',
+          },
+          -- List of LSP names to ignore (e.g., `null-ls`):
+          ignore_lsp = {},
+        }
+      }
+    }
+<
+
+
 ------------------------------------------------------------------------------
 
 TABLINE ~
@@ -1097,6 +1122,14 @@ If you want to extend lualine with plugins or want to know which ones already
 do, wiki/plugins <https://github.com/nvim-lualine/lualine.nvim/wiki/Plugins> is
 for you.
 
+SUPPORT ~
+
+If you find this project useful, consider supporting development:
+
+<a href="https://buymeacoffee.com/shadmansalj" target="_blank"> <img
+src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a
+Coffee" width="200"> </a>
+
 Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
 
 vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/examples/bubbles.lua b/examples/bubbles.lua
index 88edf57..2cd76fa 100644
--- a/examples/bubbles.lua
+++ b/examples/bubbles.lua
@@ -41,7 +41,7 @@ require('lualine').setup {
     lualine_a = { { 'mode', separator = { left = '' }, right_padding = 2 } },
     lualine_b = { 'filename', 'branch' },
     lualine_c = {
-      '%=', --[[ add your center compoentnts here in place of this comment ]]
+      '%=', --[[ add your center components here in place of this comment ]]
     },
     lualine_x = {},
     lualine_y = { 'filetype', 'progress' },
diff --git a/lua/lualine.lua b/lua/lualine.lua
index bcfb5cb..e68eca8 100644
--- a/lua/lualine.lua
+++ b/lua/lualine.lua
@@ -186,20 +186,22 @@ local statusline = modules.utils.retry_call_wrap(function(sections, is_focused,
 end)
 
 --- check if any extension matches the filetype and return proper sections
----@param current_ft string : filetype name of current file
+---@param current_ft_list string[] : filetype name of current file
 ---@param is_focused boolean : whether being evaluated for focused window or not
 ---@return table|nil : (section_table) section config where components are replaced with
 ---      component objects
 -- TODO: change this so it uses a hash table instead of iteration over list
 --       to improve redraws. Add buftype / bufname for extensions
 --       or some kind of cond ?
-local function get_extension_sections(current_ft, is_focused, sec_name)
-  for _, extension in ipairs(config.extensions) do
-    if vim.tbl_contains(extension.filetypes, current_ft) then
-      if is_focused then
-        return extension[sec_name]
-      else
-        return extension['inactive_' .. sec_name] or extension[sec_name]
+local function get_extension_sections(current_ft_list, is_focused, sec_name)
+  for _, ft in ipairs(current_ft_list) do
+    for _, extension in ipairs(config.extensions) do
+      if vim.tbl_contains(extension.filetypes, ft) then
+        if is_focused then
+          return extension[sec_name]
+        else
+          return extension['inactive_' .. sec_name] or extension[sec_name]
+        end
       end
     end
   end
@@ -246,8 +248,20 @@ local function setup_theme()
       -- use the provided theme as-is
       return config.options.theme
     elseif type(theme_name) == 'function' then
-      -- call function and use returned (dynamic) theme as-is
-      return config.options.theme()
+      -- call function and use returned (dyanmic) theme, either as-is or as a string
+      local ok, dynamic_theme = pcall(theme_name)
+      if ok and (type(dynamic_theme) == 'string') then
+        local ok_string, theme = pcall(modules.loader.load_theme, dynamic_theme)
+        if ok_string and theme then
+          return theme
+        end
+      elseif ok and (type(dynamic_theme) == 'table') then
+        return dynamic_theme
+      else
+        local error_message = 'Invalid theme type returned from function: ' .. type(dynamic_theme)
+        notify_theme_error(error_message)
+        return dynamic_theme
+      end
     end
     if theme_name ~= 'auto' then
       notify_theme_error(theme_name)
@@ -278,17 +292,17 @@ local function status_dispatch(sec_name)
     local current_ft = refresh_real_curwin
         and vim.api.nvim_buf_get_option(vim.api.nvim_win_get_buf(refresh_real_curwin), 'filetype')
       or vim.bo.filetype
+    local current_ft_list = vim.split(current_ft, '%.') -- handle compound filetypes c.doxygen
     local is_focused = focused ~= nil and focused or modules.utils.is_focused()
-    if
-      vim.tbl_contains(
-        config.options.disabled_filetypes[(sec_name == 'sections' and 'statusline' or sec_name)],
-        current_ft
-      )
-    then
-      -- disable on specific filetypes
-      return nil
+    for _, ft in ipairs(current_ft_list) do
+      if
+        vim.tbl_contains(config.options.disabled_filetypes[(sec_name == 'sections' and 'statusline' or sec_name)], ft)
+      then
+        -- disable on specific filetypes
+        return nil
+      end
     end
-    local extension_sections = get_extension_sections(current_ft, is_focused, sec_name)
+    local extension_sections = get_extension_sections(current_ft_list, is_focused, sec_name)
     if extension_sections ~= nil then
       retval = statusline(extension_sections, is_focused, sec_name == 'winbar')
     else
@@ -436,7 +450,7 @@ local function set_tabline(hide)
       0,
       config.options.refresh.tabline,
       modules.utils.timer_call(timers.tal_timer, 'lualine_tal_refresh', function()
-        refresh { kind = 'tabpage', place = { 'tabline' }, trigger = 'timer' }
+        refresh { scope = 'tabpage', place = { 'tabline' }, trigger = 'timer' }
       end, 3, 'lualine: Failed to refresh tabline')
     )
     modules.nvim_opts.set('showtabline', config.options.always_show_tabline and 2 or 1, { global = true })
@@ -444,7 +458,7 @@ local function set_tabline(hide)
     vim.schedule(function()
       -- imediately refresh upon load
       -- schedule needed so stuff like filetype detect can run first
-      refresh { kind = 'tabpage', place = { 'tabline' }, trigger = 'init' }
+      refresh { scope = 'tabpage', place = { 'tabline' }, trigger = 'init' }
     end)
   else
     modules.nvim_opts.restore('tabline', { global = true })
@@ -469,7 +483,7 @@ local function set_statusline(hide)
         0,
         config.options.refresh.statusline,
         modules.utils.timer_call(timers.stl_timer, 'lualine_stl_refresh', function()
-          refresh { kind = 'window', place = { 'statusline' }, trigger = 'timer' }
+          refresh { scope = 'window', place = { 'statusline' }, trigger = 'timer' }
         end, 3, 'lualine: Failed to refresh statusline')
       )
     else
@@ -479,7 +493,7 @@ local function set_statusline(hide)
         0,
         config.options.refresh.statusline,
         modules.utils.timer_call(timers.stl_timer, 'lualine_stl_refresh', function()
-          refresh { kind = 'tabpage', place = { 'statusline' }, trigger = 'timer' }
+          refresh { scope = 'tabpage', place = { 'statusline' }, trigger = 'timer' }
         end, 3, 'lualine: Failed to refresh statusline')
       )
     end
@@ -488,9 +502,9 @@ local function set_statusline(hide)
       -- imediately refresh upon load
       -- schedule needed so stuff like filetype detect can run first
       if config.options.globalstatus then
-        refresh { kind = 'window', place = { 'statusline' }, trigger = 'init' }
+        refresh { scope = 'window', place = { 'statusline' }, trigger = 'init' }
       else
-        refresh { kind = 'tabpage', place = { 'statusline' }, trigger = 'init' }
+        refresh { scope = 'tabpage', place = { 'statusline' }, trigger = 'init' }
       end
     end)
   else
@@ -513,14 +527,14 @@ local function set_winbar(hide)
       0,
       config.options.refresh.winbar,
       modules.utils.timer_call(timers.wb_timer, 'lualine_wb_refresh', function()
-        refresh { kind = 'tabpage', place = { 'winbar' }, trigger = 'timer' }
+        refresh { scope = 'tabpage', place = { 'winbar' }, trigger = 'timer' }
       end, 3, 'lualine: Failed to refresh winbar')
     )
     timers.halt_wb_refresh = false
     vim.schedule(function()
       -- imediately refresh upon load.
       -- schedule needed so stuff like filetype detect can run first
-      refresh { kind = 'tabpage', place = { 'winbar' }, trigger = 'init' }
+      refresh { scope = 'tabpage', place = { 'winbar' }, trigger = 'init' }
     end)
   elseif vim.fn.has('nvim-0.8') == 1 then
     modules.nvim_opts.restore('winbar', { global = true })
diff --git a/lua/lualine/component.lua b/lua/lualine/component.lua
index a42f589..4d3fcfe 100644
--- a/lua/lualine/component.lua
+++ b/lua/lualine/component.lua
@@ -105,7 +105,13 @@ function M:apply_padding()
     end
   end
   if r_padding then
-    self.status = self.status .. string.rep(' ', r_padding)
+    if self.status:reverse():find('%*%%.*#.*#%%') == 1 then
+      -- When component has changed the highlight at the end
+      -- we will add the padding before the highlight terminates
+      self.status = self.status:sub(1, -3) .. string.rep(' ', r_padding) .. self.status:sub(-2, -1)
+    else
+      self.status = self.status .. string.rep(' ', r_padding)
+    end
   end
 end
 
diff --git a/lua/lualine/components/filename.lua b/lua/lualine/components/filename.lua
index 22fd964..9189c7f 100644
--- a/lua/lualine/components/filename.lua
+++ b/lua/lualine/components/filename.lua
@@ -21,7 +21,10 @@ local default_options = {
 
 local function is_new_file()
   local filename = vim.fn.expand('%')
-  return filename ~= '' and vim.bo.buftype == '' and vim.fn.filereadable(filename) == 0
+  return filename ~= ''
+    and filename:match('^%a+://') == nil
+    and vim.bo.buftype == ''
+    and vim.fn.filereadable(filename) == 0
 end
 
 ---shortens path by turning apple/orange -> a/orange
diff --git a/lua/lualine/components/lsp_status.lua b/lua/lualine/components/lsp_status.lua
new file mode 100644
index 0000000..216e6ab
--- /dev/null
+++ b/lua/lualine/components/lsp_status.lua
@@ -0,0 +1,103 @@
+local lualine_require = require('lualine_require')
+
+local M = lualine_require.require('lualine.component'):extend()
+
+local default_options = {
+  icon = '', -- f013
+  symbols = {
+    -- Use standard unicode characters for the spinner and done symbols:
+    spinner = { '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' },
+    done = '✓',
+    separator = ' ',
+  },
+  -- List of LSP names to ignore (e.g., `null-ls`):
+  ignore_lsp = {},
+}
+
+---Advances the spinner every 80ms (or just once if precise time measurement is not supported) and returns the new
+---spinner symbol.
+function M:__advance_spinner()
+  if vim.uv then
+    self.__spinner_index = math.floor(vim.uv.hrtime() / (1e6 * 80))
+  else
+    self.__spinner_index = (self.__spinner_index or 0) + 1
+  end
+  self.__spinner_index = self.__spinner_index % #self.symbols.spinner
+  -- The spinner symbols table is 1-indexed.
+  return self.symbols.spinner[self.__spinner_index + 1]
+end
+
+function M:init(options)
+  -- Run `super()`.
+  M.super.init(self, options)
+
+  -- Apply default options.
+  self.options = vim.tbl_deep_extend('keep', self.options or {}, default_options)
+
+  -- Apply symbols.
+  self.symbols = self.options.symbols or {}
+
+  ---The difference between the `begin` and `end` progress events for each LSP.
+  ---
+  ---@type table<integer, integer>
+  self.lsp_work_by_client_id = {}
+
+  -- Listen to progress updates only if `nvim` supports the `LspProgress` event.
+  pcall(vim.api.nvim_create_autocmd, 'LspProgress', {
+    desc = 'Update the Lualine LSP status component with progress',
+    group = vim.api.nvim_create_augroup('lualine_lsp_progress', {}),
+    ---@param event {data: {client_id: integer, params: lsp.ProgressParams}}
+    callback = function(event)
+      local kind = event.data.params.value.kind
+      local client_id = event.data.client_id
+
+      local work = self.lsp_work_by_client_id[client_id] or 0
+      local work_change = kind == 'begin' and 1 or (kind == 'end' and -1 or 0)
+
+      self.lsp_work_by_client_id[client_id] = math.max(work + work_change, 0)
+
+      -- Refresh Lualine to update the LSP status symbol if it changed.
+      if (work == 0 and work_change > 0) or (work == 1 and work_change < 0) then
+        require('lualine').refresh()
+      end
+    end,
+  })
+end
+
+function M:update_status()
+  local bufnr = vim.api.nvim_get_current_buf()
+  local result = {}
+
+  -- Retrieve the active LSPs in the current buffer in a backwards-compatible way.
+  local clients
+  if vim.lsp.get_clients then
+    clients = vim.lsp.get_clients { bufnr = bufnr }
+  else
+    ---@diagnostic disable-next-line: deprecated
+    clients = vim.lsp.get_active_clients { bufnr = bufnr }
+  end
+
+  -- Advance the spinner only once, not for each client (otherwise the spinners will skip steps).
+  local spinner_symbol = self:__advance_spinner()
+
+  for _, client in ipairs(clients) do
+    local status
+    local work = self.lsp_work_by_client_id[client.id]
+    if work ~= nil and work > 0 then
+      status = spinner_symbol
+    elseif work ~= nil and work == 0 then
+      status = self.symbols.done
+    end
+
+    -- Backwards-compatible function to check if a list contains a value.
+    local list_contains = vim.list_contains or vim.tbl_contains
+
+    -- Append the status to the LSP only if it supports progress reporting and is not ignored.
+    if not list_contains(self.options.ignore_lsp, client.name) then
+      table.insert(result, client.name .. (status and ' ' .. status or ''))
+    end
+  end
+  return table.concat(result, self.symbols.separator)
+end
+
+return M
diff --git a/lua/lualine/extensions/trouble.lua b/lua/lualine/extensions/trouble.lua
index 1f79ecf..e39ece1 100644
--- a/lua/lualine/extensions/trouble.lua
+++ b/lua/lualine/extensions/trouble.lua
@@ -1,9 +1,10 @@
 local M = {}
 
-local function get_trouble_mode()
-  local opts = require('trouble.config').options
-
-  local words = vim.split(opts.mode, '[%W]')
+---Format mode, eg: lsp_document_symbols -> Lsp Document Symbols
+---@param mode string
+---@return string
+local function _format_mode(mode)
+  local words = vim.split(mode, '[%W]')
   for i, word in ipairs(words) do
     words[i] = word:sub(1, 1):upper() .. word:sub(2)
   end
@@ -11,12 +12,29 @@ local function get_trouble_mode()
   return table.concat(words, ' ')
 end
 
+local function get_trouble_mode()
+  local opts = require('trouble.config').options
+  if opts ~= nil and opts.mode ~= nil then
+    return _format_mode(opts.mode)
+  end
+
+  local win = vim.api.nvim_get_current_win()
+  if vim.w[win] ~= nil then
+    local trouble = vim.w[win].trouble
+    if trouble ~= nil and trouble.mode ~= nil then
+      return _format_mode(trouble.mode)
+    end
+  end
+
+  return ''
+end
+
 M.sections = {
   lualine_a = {
     get_trouble_mode,
   },
 }
 
-M.filetypes = { 'Trouble' }
+M.filetypes = { 'trouble', 'Trouble' }
 
 return M
diff --git a/lua/lualine/themes/base16.lua b/lua/lualine/themes/base16.lua
index daf6a6b..11c31b6 100644
--- a/lua/lualine/themes/base16.lua
+++ b/lua/lualine/themes/base16.lua
@@ -106,6 +106,21 @@ local function setup_base16_vim()
     }
   end
 
+  -- base16-vim has been renamed to tinted-vim along with colors
+  -- context: https://github.com/nvim-lualine/lualine.nvim/pull/1352
+  if vim.g.tinted_gui00 and vim.g.tinted_gui0F then
+    return setup {
+      bg = vim.g.tinted_gui01,
+      alt_bg = vim.g.tinted_gui02,
+      dark_fg = vim.g.tinted_gui03,
+      fg = vim.g.tinted_gui04,
+      light_fg = vim.g.tinted_gui05,
+      normal = vim.g.tinted_gui0D,
+      insert = vim.g.tinted_gui0B,
+      visual = vim.g.tinted_gui0E,
+      replace = vim.g.tinted_gui09,
+    }
+  end
   return nil
 end
 


Test patch:
diff --git a/tests/helpers.lua b/tests/helpers.lua
index 204cc2f..c4a9c4a 100644
--- a/tests/helpers.lua
+++ b/tests/helpers.lua
@@ -74,6 +74,9 @@ M.P = function(t)
 end
 
 function M.dedent(str, leave_indent)
+  if str == nil then
+    return nil
+  end
   -- find minimum common indent across lines
   local indent = nil
   for line in str:gmatch('[^\n]+') do
diff --git a/tests/spec/component_spec.lua b/tests/spec/component_spec.lua
index 3311b95..85dcd5b 100644
--- a/tests/spec/component_spec.lua
+++ b/tests/spec/component_spec.lua
@@ -40,12 +40,9 @@ describe('Component:', function()
     -- color highlight wan't in options when create_comp_hl was
     -- called so remove it before assert
     comp1.options.color_highlight = nil
-    assert.stub(hl.create_component_highlight_group).was_called_with(
-      color,
-      comp1.options.component_name,
-      comp1.options,
-      false
-    )
+    assert
+      .stub(hl.create_component_highlight_group)
+      .was_called_with(color, comp1.options.component_name, comp1.options, false)
     hl.create_component_highlight_group:revert()
     color = 'MyHl'
     local opts2 = build_component_opts { color = color }
@@ -56,12 +53,9 @@ describe('Component:', function()
     -- color highlight wan't in options when create_comp_hl was
     -- called so remove it before assert
     comp2.options.color_highlight = nil
-    assert.stub(hl.create_component_highlight_group).was_called_with(
-      color,
-      comp2.options.component_name,
-      comp2.options,
-      false
-    )
+    assert
+      .stub(hl.create_component_highlight_group)
+      .was_called_with(color, comp2.options.component_name, comp2.options, false)
     hl.create_component_highlight_group:revert()
   end)
 
@@ -261,7 +255,7 @@ describe('Encoding component', function()
     local opts = build_component_opts {
       component_separators = { left = '', right = '' },
       padding = 0,
-      show_bomb = true
+      show_bomb = true,
     }
     local tmp_path = 'tmp.txt'
     local tmp_fp = io.open(tmp_path, 'w')
@@ -357,12 +351,9 @@ describe('Filetype component', function()
     assert_component('filetype', opts, '%#MyCompHl_normal#* %#lualine_c_normal#lua%#lualine_c_normal#')
     assert.stub(devicons.get_icon).was_called_with('test.lua')
     assert.stub(utils.extract_highlight_colors).was_called_with('test_highlight_group', 'fg')
-    assert.stub(hl.create_component_highlight_group).was_called_with(
-      { fg = '#000' },
-      'filetype_test_highlight_group',
-      opts,
-      false
-    )
+    assert
+      .stub(hl.create_component_highlight_group)
+      .was_called_with({ fg = '#000' }, 'filetype_test_highlight_group', opts, false)
     assert.stub(vim.fn.expand).was_called_with('%:t')
 
     devicons.get_icon:revert()
@@ -455,7 +446,7 @@ describe('Filetype component', function()
       padding = 0,
       colored = false,
       icon_only = false,
-      icon = { align = 'right' }
+      icon = { align = 'right' },
     }
     assert_component('filetype', opts, 'lua * ')
     assert.stub(devicons.get_icon).was_called_with('test.lua')
@@ -528,7 +519,7 @@ describe('Location component', function()
     assert_component('location', opts, '  1:1 ')
     vim.cmd('normal! 9o')
     assert_component('location', opts, ' 10:1 ')
-    vim.api.nvim_win_set_cursor(0, {5, 0})
+    vim.api.nvim_win_set_cursor(0, { 5, 0 })
     assert_component('location', opts, '  5:1 ')
     -- test column number
     vim.cmd('normal! oTest')
@@ -549,7 +540,7 @@ describe('Progress component', function()
     assert_component('progress', opts, 'Top')
     vim.cmd('normal! 9o')
     assert_component('progress', opts, 'Bot')
-    vim.api.nvim_win_set_cursor(0, {5, 0})
+    vim.api.nvim_win_set_cursor(0, { 5, 0 })
     assert_component('progress', opts, '50%%')
     vim.cmd('bdelete!')
   end)
@@ -617,7 +608,7 @@ describe('Filename component', function()
     vim.cmd(':bdelete!')
   end)
 
-  it('can show new_file_status', function ()
+  it('can show new_file_status', function()
     local opts = build_component_opts {
       component_separators = { left = '', right = '' },
       padding = 0,
@@ -872,3 +863,139 @@ describe('Branch component', function()
     assert_component('branch', opts, rev)
   end)
 end)
+
+describe('lsp_status component', function()
+  local assert_comp_ins = helpers.assert_component_instance
+  local tmpdir
+  local file
+  local opts = build_component_opts {
+    component_separators = { left = '', right = '' },
+    padding = 0,
+    ignore_lsp = { 'null-ls' },
+  }
+  local lsp_status_comp
+
+  before_each(function()
+    require('lualine').setup {}
+    tmpdir = os.tmpname()
+    file = os.tmpname() .. '/test.lua'
+    vim.cmd([[
+      augroup lualine
+        autocmd!
+      augroup END
+    ]])
+    lsp_status_comp = helpers.init_component('lsp_status', opts)
+  end)
+
+  after_each(function()
+    os.remove(tmpdir)
+  end)
+
+  it('is empty when no LSP', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns {}
+
+    assert_comp_ins(lsp_status_comp, '')
+  end)
+
+  it('is empty when LSP is ignored', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns {
+      { id = 2, name = 'null-ls' },
+    }
+
+    assert_comp_ins(lsp_status_comp, '')
+  end)
+
+  it('shows LSP name and icon when attached', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns { { id = 2, name = 'lua_ls' } }
+
+    vim.cmd('edit ' .. file)
+
+    assert_comp_ins(lsp_status_comp, ' lua_ls')
+  end)
+
+  it('shows LSP name and icon in older nvim version', function()
+    vim.cmd('edit ' .. file)
+    vim.lsp.get_clients = nil
+    stub(vim.lsp, 'get_active_clients')
+    ---@diagnostic disable-next-line: deprecated
+    vim.lsp.get_active_clients
+      .on_call_with({ bufnr = vim.api.nvim_get_current_buf() })
+      .returns { { id = 2, name = 'lua_ls' } }
+
+    assert_comp_ins(lsp_status_comp, ' lua_ls')
+  end)
+
+  it('shows LSP progress when supported with precise time measurement', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns { { id = 2, name = 'lua_ls' } }
+    vim.uv = vim.uv or {}
+    stub(vim.uv, 'hrtime')
+    vim.uv.hrtime.on_call_with().returns(12 * 1e6 * 80)
+
+    local ok = pcall(vim.api.nvim_exec_autocmds, 'LspProgress', {
+      data = { client_id = 2, params = { value = { kind = 'begin' } } },
+    })
+
+    -- Skip assertion if LSP progress updates are not supported by the current `nvim` version.
+    if ok then
+      -- Use spinner symbol 12 (time) % 10 (symbol table size) = 2.
+      assert_comp_ins(lsp_status_comp, ' lua_ls ⠹')
+    end
+  end)
+
+  it('shows LSP progress when supported without precise time measurement', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns { { id = 2, name = 'lua_ls' } }
+    vim.uv = nil
+
+    local ok = pcall(vim.api.nvim_exec_autocmds, 'LspProgress', {
+      data = { client_id = 2, params = { value = { kind = 'begin' } } },
+    })
+
+    -- Skip assertion if LSP progress updates are not supported by the current `nvim` version.
+    if ok then
+      -- We start from 0 and advanced by 1, so use spinner symbol 1.
+      assert_comp_ins(lsp_status_comp, ' lua_ls ⠙')
+    end
+  end)
+
+  it('shows LSP progress when supported with precise time measurement', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns { { id = 2, name = 'lua_ls' } }
+
+    local ok = pcall(vim.api.nvim_exec_autocmds, 'LspProgress', {
+      data = { client_id = 2, params = { value = { kind = 'end' } } },
+    })
+
+    -- Skip assertion if LSP progress updates are not supported by the current `nvim` version.
+    if ok then
+      assert_comp_ins(lsp_status_comp, ' lua_ls ✓')
+    end
+  end)
+
+  it('shows LSP done when supported', function()
+    vim.cmd('edit ' .. file)
+    stub(vim.lsp, 'get_clients')
+    vim.lsp.get_clients.on_call_with({ bufnr = vim.api.nvim_get_current_buf() }).returns { { id = 2, name = 'lua_ls' } }
+
+    local ok = pcall(vim.api.nvim_exec_autocmds, 'LspProgress', {
+      data = { client_id = 2, params = { value = { kind = 'begin' } } },
+    }) and pcall(vim.api.nvim_exec_autocmds, 'LspProgress', {
+      data = { client_id = 2, params = { value = { kind = 'end' } } },
+    })
+
+    -- Skip assertion if LSP progress updates are not supported by the current `nvim` version.
+    if ok then
+      assert_comp_ins(lsp_status_comp, ' lua_ls ✓')
+    end
+  end)
+end)
diff --git a/tests/spec/config_spec.lua b/tests/spec/config_spec.lua
index ba7532a..890c496 100644
--- a/tests/spec/config_spec.lua
+++ b/tests/spec/config_spec.lua
@@ -86,12 +86,12 @@ describe('config parsing', function()
     describe('disabled filetypes', function()
       it('default', function()
         local config = config_module.apply_configuration {}
-        eq(config.options.disabled_filetypes, {statusline={}, winbar={}})
+        eq(config.options.disabled_filetypes, { statusline = {}, winbar = {} })
       end)
       it('custom', function()
         local config = { options = { disabled_filetypes = { 'lua' } } }
         config = config_module.apply_configuration(config)
-        eq(config.options.disabled_filetypes, {statusline={'lua'}, winbar={'lua'}})
+        eq(config.options.disabled_filetypes, { statusline = { 'lua' }, winbar = { 'lua' } })
       end)
     end)
 
diff --git a/tests/spec/lualine_spec.lua b/tests/spec/lualine_spec.lua
index 7833ea5..a067b69 100644
--- a/tests/spec/lualine_spec.lua
+++ b/tests/spec/lualine_spec.lua
@@ -1027,4 +1027,45 @@ describe('Lualine', function()
       ]===])
     end)
   end)
+
+  describe('supports compound filetypes', function()
+    it('disabled filetypes', function()
+      local conf = require('lualine').get_config()
+      conf.options.disabled_filetypes = { 'java' }
+      require('lualine').setup(conf)
+      local old_ft = vim.bo.ft
+      vim.bo.ft = 'lua.java'
+      vim.opt.filetype = 'lua.java'
+      statusline:expect(nil)
+      vim.bo.ft = old_ft
+    end)
+
+    it('extensions for work on compound filetypes', function()
+      local conf = require('lualine').get_config()
+      table.insert(conf.extensions, {
+        filetypes = { 'test_ft' },
+        sections = {
+          lualine_a = {
+            function()
+              return 'custom_extension_component'
+            end,
+          },
+        },
+      })
+      local old_ft = vim.bo.ft
+      vim.bo.ft = 'lua.test_ft'
+      require('lualine').setup(conf)
+      statusline:expect([===[
+      highlights = {
+          1: lualine_a_normal = { bg = "#a89984", bold = true, fg = "#282828" }
+          2: lualine_transitional_lualine_a_normal_to_lualine_c_normal = { bg = "#3c3836", fg = "#a89984" }
+          3: lualine_c_normal = { bg = "#3c3836", fg = "#a89984" }
+      }
+      |{1: custom_extension_component }
+      {2:}
+      {3:                                                                                           }|
+      ]===])
+      vim.bo.ft = old_ft
+    end)
+  end)
 end)
diff --git a/tests/spec/utils_spec.lua b/tests/spec/utils_spec.lua
index 30654b7..8491a9a 100644
--- a/tests/spec/utils_spec.lua
+++ b/tests/spec/utils_spec.lua
@@ -37,21 +37,28 @@ describe('Utils', function()
     vim.cmd(string.format('hi hl_std guifg=%s guibg=%s', hl_std.fg, hl_std.bg))
     vim.cmd(string.format('hi hl_rvs guifg=%s guibg=%s gui=reverse', hl_rvs.fg, hl_rvs.bg))
     vim.cmd(string.format('hi hl_ul guisp=%s gui=undercurl', hl_ul.sp))
-    vim.cmd(string.format('hi hl_ul_rvs guifg=%s guibg=%s guisp=%s gui=reverse,undercurl', hl_ul_rvs.fg, hl_ul_rvs.bg, hl_ul_rvs.sp))
+    vim.cmd(
+      string.format(
+        'hi hl_ul_rvs guifg=%s guibg=%s guisp=%s gui=reverse,undercurl',
+        hl_ul_rvs.fg,
+        hl_ul_rvs.bg,
+        hl_ul_rvs.sp
+      )
+    )
     -- Can extract color from primary highlight group
-    eq(utils.extract_color_from_hllist('fg', {'hl_std','hl_ul'}, def_clr), fg_clr)
+    eq(utils.extract_color_from_hllist('fg', { 'hl_std', 'hl_ul' }, def_clr), fg_clr)
     -- Can extract color from fallback highlight group
-    eq(utils.extract_color_from_hllist('fg', {'hl_noexist','hl_std'}, def_clr), fg_clr)
+    eq(utils.extract_color_from_hllist('fg', { 'hl_noexist', 'hl_std' }, def_clr), fg_clr)
     -- Can fall back to default color on nonexistent color
-    eq(utils.extract_color_from_hllist('fg', {'hl_ul'}, def_clr), def_clr)
+    eq(utils.extract_color_from_hllist('fg', { 'hl_ul' }, def_clr), def_clr)
     -- Can fall back to default color on nonexistent highlight group
-    eq(utils.extract_color_from_hllist('fg', {'hl_noexist'}, def_clr), def_clr)
+    eq(utils.extract_color_from_hllist('fg', { 'hl_noexist' }, def_clr), def_clr)
     -- Can extract fallback color
-    eq(utils.extract_color_from_hllist({'fg','sp'}, {'hl_ul'}, def_clr), sp_clr)
+    eq(utils.extract_color_from_hllist({ 'fg', 'sp' }, { 'hl_ul' }, def_clr), sp_clr)
     -- Can extract reverse color
-    eq(utils.extract_color_from_hllist('fg', {'hl_rvs'}, def_clr), bg_clr)
+    eq(utils.extract_color_from_hllist('fg', { 'hl_rvs' }, def_clr), bg_clr)
     -- Can extract fallback reverse color
-    eq(utils.extract_color_from_hllist({'sp','fg'}, {'hl_rvs'}, def_clr), bg_clr)
+    eq(utils.extract_color_from_hllist({ 'sp', 'fg' }, { 'hl_rvs' }, def_clr), bg_clr)
     -- clear highlights
     vim.cmd('hi clear hl_std')
     vim.cmd('hi clear hl_rvs')
diff --git a/tests/statusline.lua b/tests/statusline.lua
index 00e1855..8ee21d4 100644
--- a/tests/statusline.lua
+++ b/tests/statusline.lua
@@ -78,6 +78,9 @@ local M = {}
 
 local function eval_stl(stl_expr, width, eval_type)
   local stl_buf, hl_list, stl_eval_res
+  if stl_expr == nil then
+    return nil
+  end
   stl_eval_res = vim.api.nvim_eval_statusline(
     stl_expr,
     { maxwidth = width, highlights = true, fillchar = ' ', use_tabline = (eval_type == 'tabline') }
@@ -123,12 +126,16 @@ function M:expect_expr(expect, expr)
     assert.are.same(expect, nil)
     return
   end
-  expect = helpers.dedent(expect)
   local actual = eval_stl(expr, self.width, self.type)
+  if expect == nil then
+    assert.are.same(expect, actual)
+    return
+  end
+  expect = helpers.dedent(expect)
   local matched = true
   local errmsg = {}
   if expect ~= actual then
-    expect = vim.split(expect, '\n')
+    expect = expect ~= nil and vim.split(expect, '\n')
     actual = vim.split(actual, '\n')
     if expect[#expect] == '' then
       expect[#expect] = nil
@@ -171,6 +178,10 @@ function M:snapshot_expr(expr)
     inactive = 'inactive_statusline',
     tabline = 'tabline',
   }
+  if expr == nil then
+    print((type_map[self.type] or 'statusline') .. ':expect(nil)')
+    return
+  end
   print((type_map[self.type] or 'statusline') .. ':expect([===[')
   print(eval_stl(expr, self.width, self.type) .. ']===])')
 end