rcarriga / nvim-notify

A fancy, configurable, notification manager for NeoVim
MIT License
3k stars 80 forks source link

LSP integration sample: integrate with neovim's progress report #70

Closed vogelsgesang closed 2 years ago

vogelsgesang commented 2 years ago

Turns out neovim already has its own $/progress handler. We should probably not override it, but listen to the autocommand. Also see https://github.com/j-hui/fidget.nvim/issues/51

(Not part of the plugin itself, only regarding code in the wiki. Leaving this here for discussion. Maybe I will fix this myself some time later)

rcarriga commented 2 years ago

Ah good point, feel free to update or I'll get to it when I have a chance :ok_hand:

jokeyrhyme commented 2 years ago

Howdie folks,

I thought I'd give this a try, but I'm a bit stuck

❯ nvim --version
NVIM v0.7.0

I've inserted the following right next where I've but the current snippet from the wiki, but the print() never happens, even when I can see notifications from the wiki snippet just fine

vim.api.nvim_create_autocmd({ "User" }, {
  desc = "show progress as notifications",
  callback = function(opts)
    print(vim.inspect(opts))
  end,
  pattern = { "LspProgressUpdate" },
})

If I change the pattern to "*" then I can see "LspRequest" events coming through, just never "LspProgressUpdate" events

Do I need to do something specific to enable the LspProgressUpdates? I think I'm using a new-enough nvim, but is this only in nightly or something maybe?

Cheers!

mehalter commented 2 years ago

I took a stab at this this morning and got a pretty good solution I think:

local client_notifs = {}
local spinner_frames = { "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" } -- spinners

local function update_spinner(notif_data) -- update spinner helper function to defer
  if notif_data.spinner then
    local new_spinner = (notif_data.spinner + 1) % #spinner_frames
    notif_data.spinner = new_spinner

    notif_data.notification = vim.notify(nil, nil, {
      hide_from_history = true,
      icon = spinner_frames[new_spinner],
      replace = notif_data.notification,
    })

    vim.defer_fn(function()
      update_spinner(notif_data)
    end, 100)
  end
end

vim.api.nvim_create_augroup("lsp_notify", { clear = true }) -- create augroup
vim.api.nvim_create_autocmd("User", {
  pattern = "LspProgressUpdate", -- LSP Progress Update autocommand
  group = "lsp_notify",
  desc = "LSP progress notification",
  callback = function()
    local bufnr = vim.api.nvim_get_current_buf() -- get buffer number
    for _, client in pairs(vim.lsp.buf_get_clients()) do -- loop over each client to check notifications
      if not client_notifs[bufnr] then -- create buffer specific notification table if not exists
        client_notifs[bufnr] = {}
      end
      if not client_notifs[bufnr][client.id] then -- create buffers client  specific  notification table if not exists
        client_notifs[bufnr][client.id] = {}
      end
      local notif_data = client_notifs[bufnr][client.id] -- set notif_data variable
      local progress = nil
      for _, progress_msg in pairs(client.messages.progress) do
        progress = true -- expose if a progress exists
        if not progress_msg.done then
          progress = progress_msg -- get clients first not done progress messages
          break
        end
      end
      if type(progress) == "table" and progress.percentage and progress.percentage ~= 0 then -- if there is a progress message
        local notify_opts = {} -- define notification options
        local new_msg = notif_data.notification == nil -- if it's a new message set different options
        if new_msg then -- for new messages set a title, initialize icone and disable timeout
          notify_opts = {
            title = client.name .. (#progress.title > 0 and ": " .. progress.title or ""),
            icon = spinner_frames[1],
            timeout = false,
          }
        else -- for existing messages just update the existing notification
          notify_opts = { replace = notif_data.notification }
        end
        notif_data.notification = vim.notify( -- notify with percentage and message
          (progress.percentage and progress.percentage .. "%\t" or "") .. (progress.message or ""),
          "info",
          notify_opts
        )
        if new_msg then -- if it's a new message, start the update spinner background job
          update_spinner(notif_data)
        end
      elseif progress and not vim.tbl_isempty(notif_data) then -- if there is finished progress and a notification, complete it
        notif_data.notification = vim.notify(
          "Complete",
          "info",
          { icon = "", replace = notif_data.notification, timeout = 3000 }
        )
        notif_data = {} -- clear notification data
      end
    end
  end,
})
rcarriga commented 2 years ago

LTGM, feel free to replace the existing implementation :smile:

jan-xyz commented 2 years ago

I tried it and it looks good, but it seems to struggle mit multiple progress reports like rust-analyzer has.

ZenLian commented 2 years ago

I've inserted the following right next where I've but the current snippet from the wiki, but the print() never happens, even when I can see notifications from the wiki snippet just fine

vim.api.nvim_create_autocmd({ "User" }, {
  desc = "show progress as notifications",
  callback = function(opts)
    print(vim.inspect(opts))
  end,
  pattern = { "LspProgressUpdate" },
})

If I change the pattern to "*" then I can see "LspRequest" events coming through, just never "LspProgressUpdate" events

Do I need to do something specific to enable the LspProgressUpdates? I think I'm using a new-enough nvim, but is this only in nightly or something maybe?

Cheers!

@jokeyrhyme Guess you add this without deleting the code snippet from the wiki?

LspProgressUpdate event is actually triggered in neovim's builtin "$/progress" handler. If you override vim.lsp.handlers["$/progress" ] , then this event will never be triggered.

jokeyrhyme commented 2 years ago

@ZenLian ah, yes, you're absolutely right, haha, thanks! <3

mabuchner commented 2 years ago

@mehalter the spinner isn't spinning, because notif_data.spinner never gets set to any value and the update_spinner therefore never actually does something.

I guess, notif_data.spinner = 1 should happen for the first progress notification.

mabuchner commented 2 years ago

There is also a notif_data.spinner = nil assignment missing for when the the progress is complete. The notif_data = {} assignment doesn't help, because the update_spinner function will continue to use the original object.

mitinarseny commented 2 years ago

I tweaked the implementation a bit to avoid global states, split spinner and LSP logic, and, more importantly, debounce calls to require('notify').notify() because it's too heavy and we don't need to update the contents of notification window more than, say, 10 times per second : https://github.com/mitinarseny/dotfiles/blob/4c78f07c0d046d491d086651aea6cf65ea9e6a1a/.config/nvim/after/plugin/lsp.lua#L185-L233 https://github.com/mitinarseny/dotfiles/blob/4c78f07c0d046d491d086651aea6cf65ea9e6a1a/.config/nvim/lua/spinner.lua

jan-xyz commented 2 years ago

@mitinarseny thanks! This works like a charm!

jan-xyz commented 2 years ago

@rcarriga would you like to include the code in the project? It would be nice to have a central place for it where bugs can be fixed and code shared. I feel like the wiki is not a good place to track bug fixes and it is quite a big chunk of code with potentials to break it.

rcarriga commented 2 years ago

I don't want this code to live in nvim-notify, the repo is relatively simple and I don't want to maintain use cases that I don't use. However if someone wishes to create a repo for them, I'd be happy to link them in the README!