diff --git a/example.ts b/example.ts deleted file mode 100644 index 5cec525..0000000 --- a/example.ts +++ /dev/null @@ -1,35 +0,0 @@ -let a = 12 -a = "asdf" - - - - - - - - - - - - - - - - - - - - - - - - - - - -a = "asdf" - - - - - diff --git a/flake.nix b/flake.nix index 72f51f9..1f80e0a 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,8 @@ devShell = pkgs.mkShell { name = "sense.nvim devShell"; shellHook = '' + LUA_PATH="$(luarocks path --lr-path --lua-version 5.1 --local)" + LUA_CPATH="$(luarocks path --lr-cpath --lua-version 5.1 --local)" ''; buildInputs = [ pkgs.sumneko-lua-language-server diff --git a/lua/sense/autocmds.lua b/lua/sense/autocmds.lua index 435a751..1ca5926 100644 --- a/lua/sense/autocmds.lua +++ b/lua/sense/autocmds.lua @@ -1,3 +1,4 @@ +-- TODO: lazy-load these local state = require("sense.state") local ui = require("sense.ui") local log = require("sense.log") @@ -23,6 +24,12 @@ function M.setup() :map(ui.update) end, }) + vim.api.nvim_create_autocmd({ "VimResized" }, { + callback = function() + local infos = vim.fn.getwininfo() + vim.iter(infos):map(ui.update) + end, + }) -- WinEnter: when user do `:split` -- WinScrolled: general scroll/resize events vim.api.nvim_create_autocmd({ "WinEnter", "WinScrolled", "CursorMoved" }, { diff --git a/lua/sense/helper.lua b/lua/sense/helper.lua new file mode 100644 index 0000000..0942280 --- /dev/null +++ b/lua/sense/helper.lua @@ -0,0 +1,34 @@ +local state = require("sense.state") + +local M = {} + +---@param wininfo vim.fn.getwininfo.ret.item +---@return vim.Diagnostic[] +function M.get_diags_above(wininfo) + local ds = state.diag_cache[wininfo.bufnr] or {} + ds = vim.iter(ds) + :filter(function(diag) + return diag.end_lnum + 1 < wininfo.topline + end) + :totable() + table.sort(ds, function(a, b) + -- sort in reverse order (closer comes first) + return a.end_lnum > b.end_lnum + end) + return ds +end + +function M.get_diags_below(wininfo) + local diagnostics = state.diag_cache[wininfo.bufnr] or {} + diagnostics = vim.iter(diagnostics) + :filter(function(diag) + return diag.lnum + 1 > wininfo.botline + end) + :totable() + table.sort(diagnostics, function(a, b) + return a.lnum < b.lnum + end) + return diagnostics +end + +return M diff --git a/lua/sense/ui.lua b/lua/sense/ui.lua deleted file mode 100644 index 97781de..0000000 --- a/lua/sense/ui.lua +++ /dev/null @@ -1,215 +0,0 @@ -local state = require("sense.state") - -local M = {} - ----@class sense.UI.Highlight ----@field hl_group string ----@field line integer ----@field col_start integer ----@field col_end integer - -local MAX_WIDTH = 99999 -local RIGHT_PADDING = 0 - ----Calculate max floating window size based on parent window width ----@param win integer parent window-ID -local function get_appropriate_width(win) - return math.min(MAX_WIDTH, vim.api.nvim_win_get_width(win)) - RIGHT_PADDING -end - -local function open_win(win_pos) - local win_opts = vim.tbl_deep_extend("force", win_pos, { - focusable = false, - style = "minimal", - zindex = 10, - }) - local buf = vim.api.nvim_create_buf(false, true) - local win = vim.api.nvim_open_win(buf, false, win_opts) - local winhighlight = table.concat({ - "NormalNC:Comment", - }, ",") - vim.api.nvim_set_option_value("winhighlight", winhighlight, { win = win }) - vim.api.nvim_set_option_value("winblend", 100, { win = win }) - return win, buf -end - -local virtual = { - ---@param wininfo vim.fn.getwininfo.ret.item - ---@return string[], sense.UI.Highlight - top = function(wininfo) - local diagnostics = state.diag_cache[wininfo.bufnr] or {} - ---@type vim.Diagnostic[] - diagnostics = vim.iter(diagnostics) - :filter(function(diag) - return diag.end_lnum + 1 < wininfo.topline - end) - :totable() - if #diagnostics == 0 then - return {}, {} - end - table.sort(diagnostics, function(a, b) - -- reverse sort - return a.end_lnum > b.end_lnum - end) - local cursor_row = vim.api.nvim_win_get_cursor(wininfo.winid)[1] - local distance = cursor_row - (diagnostics[1].lnum + 1) - local lines = { - ("↑ %d lines above"):format(distance), - } - local highlights = { - { - line = 0, - hl_group = "DiagnosticVirtualTextError", - col_start = 0, - col_end = -1, - }, - } - return lines, highlights - end, - ---@param wininfo vim.fn.getwininfo.ret.item - bot = function(wininfo) - local diagnostics = state.diag_cache[wininfo.bufnr] or {} - ---@type vim.Diagnostic[] - diagnostics = vim.iter(diagnostics) - :filter(function(diag) - return diag.lnum + 1 > wininfo.botline - end) - :totable() - if #diagnostics == 0 then - return {}, {} - end - table.sort(diagnostics, function(a, b) - return a.lnum < b.lnum - end) - local cursor_row = vim.api.nvim_win_get_cursor(wininfo.winid)[1] - local distance = (diagnostics[1].lnum + 1) - cursor_row - local lines = { - ("↓ %d lines below"):format(distance), - } - local highlights = { - { - line = 0, - hl_group = "DiagnosticVirtualTextError", - col_start = 0, - col_end = -1, - }, - } - return lines, highlights - end, -} - -local function set_lines(win, buf, lines, highlights) - ---@type integer[] - local offsets = {} - local width = get_appropriate_width(win) - -- Right justify each line by padding spaces - for l, line in ipairs(lines) do - local offset = width - vim.fn.strdisplaywidth(line) - lines[l] = string.rep(" ", offset) .. lines[l] - offsets[l] = offset - end - - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - - for _, highlight in ipairs(highlights) do - local offset = offsets[highlight.line + 1] - vim.api.nvim_buf_add_highlight( - buf, - 0, - highlight.hl_group, - highlight.line, - highlight.col_start + offset, - highlight.col_end == -1 and -1 or highlight.col_end + offset - ) - end -end - ----@param wininfo vim.fn.getwininfo.ret.item -function M.update_top(wininfo) - local lines, highlights = virtual.top(wininfo) - local win = vim.w[wininfo.winid].__sense_top_winid - if #lines == 0 then - if win then - vim.w[wininfo.winid].__sense_top_winid = nil - -- TODO: catch error - -- this fails on `:edit` - pcall(vim.api.nvim_win_close, win, true) - end - return - end - - ---@type vim.api.keyset.win_config - local win_pos = { - relative = "win", - win = wininfo.winid, - anchor = "NE", - row = 0, - col = vim.api.nvim_win_get_width(wininfo.winid), - width = get_appropriate_width(wininfo.winid), - height = #lines, - } - local buf - if win then - -- reposition the floating window - vim.api.nvim_win_set_config(win, win_pos) - buf = vim.api.nvim_win_get_buf(win) - else - win, buf = open_win(win_pos) - if win == 0 then - -- TODO: error occured when opening window - return - end - vim.w[wininfo.winid].__sense_top_winid = win - end - - set_lines(win, buf, lines, highlights) -end - ----@param wininfo vim.fn.getwininfo.ret.item -function M.update_bot(wininfo) - local lines, highlights = virtual.bot(wininfo) - local win = vim.w[wininfo.winid].__sense_bot_winid - if #lines == 0 then - if win then - vim.w[wininfo.winid].__sense_bot_winid = nil - -- TODO: catch error - -- this fails on `:edit` - pcall(vim.api.nvim_win_close, win, true) - end - return - end - - ---@type vim.api.keyset.win_config - local win_pos = { - relative = "win", - win = wininfo.winid, - anchor = "SE", - row = vim.api.nvim_win_get_height(wininfo.winid), - col = vim.api.nvim_win_get_width(wininfo.winid), - width = get_appropriate_width(wininfo.winid), - height = #lines, - } - local buf - if win then - -- reposition the floating window - vim.api.nvim_win_set_config(win, win_pos) - buf = vim.api.nvim_win_get_buf(win) - else - win, buf = open_win(win_pos) - if win == 0 then - -- TODO: error occured when opening window - return - end - vim.w[wininfo.winid].__sense_bot_winid = win - end - - set_lines(win, buf, lines, highlights) -end - ----@param wininfo vim.fn.getwininfo.ret.item -function M.update(wininfo) - M.update_top(wininfo) - M.update_bot(wininfo) -end - -return M diff --git a/lua/sense/ui/init.lua b/lua/sense/ui/init.lua new file mode 100644 index 0000000..c592625 --- /dev/null +++ b/lua/sense/ui/init.lua @@ -0,0 +1,33 @@ +local virtual = require("sense.ui.virtual") + +local M = {} + +---@class sense.UI.Highlight +---@field hl_group string +---@field line integer +---@field col_start integer +---@field col_end integer + +---@class sense.UI.Component +local component = {} +---@param wininfo vim.fn.getwininfo.ret.item +function component:close(wininfo) + wininfo = wininfo +end +---@param wininfo vim.fn.getwininfo.ret.item +function component:render(wininfo) + wininfo = wininfo +end + +---@param wininfo vim.fn.getwininfo.ret.item +function M.update(wininfo) + virtual.top_ui:render(wininfo) + virtual.bot_ui:render(wininfo) +end + +function M.close(wininfo) + virtual.top_ui:close(wininfo) + virtual.bot_ui:close(wininfo) +end + +return M diff --git a/lua/sense/ui/utils.lua b/lua/sense/ui/utils.lua new file mode 100644 index 0000000..0e079eb --- /dev/null +++ b/lua/sense/ui/utils.lua @@ -0,0 +1,38 @@ +local M = {} + +-- TODO: NOTE: code is from fidget.nvim + +---@param buf number +---@param lines string[] +---@param highlights sense.UI.Highlight[] +---@param width number|nil +function M.set_lines(buf, lines, highlights, width) + ---@type number[] + local offsets = {} + if width then + -- Right justify each line by padding spaces + for l, line in ipairs(lines) do + local offset = width - vim.fn.strdisplaywidth(line) + -- FIXME: handle when offset is negative + lines[l] = string.rep(" ", offset) .. lines[l] + offsets[l] = offset + end + vim.print(width) + end + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + + for _, highlight in ipairs(highlights) do + local offset = offsets[highlight.line + 1] or 0 + vim.api.nvim_buf_add_highlight( + buf, + 0, + highlight.hl_group, + highlight.line, + highlight.col_start + offset, + highlight.col_end == -1 and -1 or highlight.col_end + offset + ) + end +end + +return M diff --git a/lua/sense/ui/virtual.lua b/lua/sense/ui/virtual.lua new file mode 100644 index 0000000..2ebccf8 --- /dev/null +++ b/lua/sense/ui/virtual.lua @@ -0,0 +1,150 @@ +local helper = require("sense.helper") +local ui_utils = require("sense.ui.utils") + +local M = {} + +---@param wininfo vim.fn.getwininfo.ret.item +---@return string[], sense.UI.Highlight[] +local function render_top(wininfo) + local diagnostics = helper.get_diags_above(wininfo) + if #diagnostics == 0 then + return {}, {} + end + local closest = diagnostics[1] + local cursor_row = vim.api.nvim_win_get_cursor(wininfo.winid)[1] + local distance = cursor_row - (closest.lnum + 1) + local line = ("↑ %d lines above"):format(distance) + if closest.source then + line = line .. " [" .. closest.source .. "]" + end + local highlight = { + line = 0, + hl_group = "DiagnosticVirtualTextError", + col_start = 0, + col_end = -1, + } + return { line }, { highlight } +end + +---@param wininfo vim.fn.getwininfo.ret.item +---@return string[], sense.UI.Highlight[] +local function render_bot(wininfo) + local diagnostics = helper.get_diags_below(wininfo) + if #diagnostics == 0 then + return {}, {} + end + local closest = diagnostics[1] + local cursor_row = vim.api.nvim_win_get_cursor(wininfo.winid)[1] + local distance = (closest.lnum + 1) - cursor_row + local line = ("↓ %d lines below"):format(distance) + if closest.source then + line = line .. " [" .. closest.source .. "]" + end + local highlight = { + line = 0, + hl_group = "DiagnosticVirtualTextError", + col_start = 0, + col_end = -1, + } + return { line }, { highlight } +end + +local function open_win(win_pos) + local win_opts = vim.tbl_deep_extend("force", win_pos, { + focusable = false, + style = "minimal", + zindex = 10, + }) + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, false, win_opts) + local winhighlight = table.concat({ + "NormalNC:Comment", + }, ",") + vim.api.nvim_set_option_value("winhighlight", winhighlight, { win = win }) + vim.api.nvim_set_option_value("winblend", 100, { win = win }) + return win, buf +end + +---@param calc_pos fun(wininfo: vim.fn.getwininfo.ret.item, width: number, height: number): vim.api.keyset.win_config +---@param render_fn fun(wininfo: vim.fn.getwininfo.ret.item): string[], sense.UI.Highlight[] +---@return sense.UI.Component +local function gen_virtual_ui(name, calc_pos, render_fn) + local var_name = "__sense_nvim_" .. name + ---@param wininfo vim.fn.getwininfo.ret.item + ---@param width number + ---@param height number + ---@return number win + ---@return number buf + local function open(wininfo, width, height) + local win = vim.w[wininfo.winid][var_name] + local win_pos = calc_pos(wininfo, width, height) + local buf + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_set_config(win, win_pos) + buf = vim.api.nvim_win_get_buf(win) + else + win, buf = open_win(win_pos) + if win == 0 then + return win, buf + end + vim.w[wininfo.winid][var_name] = win + end + return win, buf + end + ---@type sense.UI.Component + return { + ---@param wininfo vim.fn.getwininfo.ret.item + close = function(_self, wininfo) + local win = vim.w[wininfo.winid][var_name] + vim.w[wininfo.winid][var_name] = nil + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + end, + ---@param wininfo vim.fn.getwininfo.ret.item + render = function(self, wininfo) + local lines, highlights = render_fn(wininfo) + if #lines == 0 then + self:close(wininfo) + return + end + local width = 0 + vim.iter(lines):map(function(line) + width = math.max(width, vim.fn.strdisplaywidth(line)) + -- truncate line if it exceeds the max width (width of parent window) + if width > wininfo.width then + -- FIXME: truncate based on displaywidth + line = line:sub(1, width / 2) + end + end) + local win, buf = open(wininfo, width, #lines) + + ui_utils.set_lines(buf, lines, highlights, vim.api.nvim_win_get_width(win)) + end, + } +end + +M.top_ui = gen_virtual_ui("top", function(wininfo, width, height) + return { + relative = "win", + win = wininfo.winid, + anchor = "NE", + row = 0, + col = vim.api.nvim_win_get_width(wininfo.winid), + width = width, + height = height, + } +end, render_top) +M.bot_ui = gen_virtual_ui("bot", function(wininfo, width, height) + return { + relative = "win", + win = wininfo.winid, + anchor = "SE", + row = vim.api.nvim_win_get_height(wininfo.winid), + col = vim.api.nvim_win_get_width(wininfo.winid), + width = width, + height = height, + } +end, render_bot) + +return M diff --git a/nix/test-overlay.nix b/nix/test-overlay.nix index f290d7f..b1c72a1 100644 --- a/nix/test-overlay.nix +++ b/nix/test-overlay.nix @@ -9,9 +9,12 @@ pname = "sense.nvim"; src = self; neovim = nvim; + luaPackages = ps: + with ps; [ + nvim-nio + ]; extraPackages = [ - wget - git + go gopls ]; diff --git a/spec/example.go b/spec/example.go index ba15186..d4665eb 100644 --- a/spec/example.go +++ b/spec/example.go @@ -3,3 +3,33 @@ package main func main() { fmt.Println("hi") } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/minimal_init.lua b/spec/minimal_init.lua index 012dca5..8fb6652 100644 --- a/spec/minimal_init.lua +++ b/spec/minimal_init.lua @@ -1,14 +1,18 @@ local test_dir = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h") -local _sense_nvim_dir = vim.fn.fnamemodify(test_dir, ":h") +local sense_nvim_dir = vim.fn.fnamemodify(test_dir, ":h") + +vim.opt.runtimepath:append(sense_nvim_dir) +vim.cmd("runtime! ftplugin.vim") +vim.cmd("runtime! ftdetect/*.{vim,lua}") +vim.cmd("runtime! filetype.lua") +vim.cmd("runtime! plugin/**/*.{vim,lua}") --- TODO: use gopls instead local servers = { gopls = { name = "gopls", cmd = { "gopls" }, root_dir = vim.fs.root(0, { "go.work", "go.mod", "gotmpl", ".git" }), filetypes = { "go", "gomod", "gowork", "gotmpl" }, - init_options = { hostInfo = "neovim" }, }, } local group = vim.api.nvim_create_augroup("UserLspStart", { clear = true }) diff --git a/spec/ui/virtual_spec.lua b/spec/ui/virtual_spec.lua index 0d69e96..4c46ac9 100644 --- a/spec/ui/virtual_spec.lua +++ b/spec/ui/virtual_spec.lua @@ -1,13 +1,28 @@ ---@module 'luassert' -describe("Test", function () +require("spec.minimal_init") +local nio = require("nio") +local ui = require("sense.ui") + +describe("Test", function() it("small test", function() - assert.same(2, 1 + 1) + assert.same(3, 1 + 2) end) - it("can I use gopls?", function () + nio.tests.it("goplsssss", function () vim.cmd.edit("spec/example.go") local buf = vim.api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ bufnr = buf }) - assert.same(1, #clients) + assert.same("go", vim.bo[buf].filetype) + -- HACK: this is clearly not good way to wait until LspAttach event + nio.sleep(500) + local client = vim.lsp.get_clients({ bufnr = buf })[1] + assert.not_nil(client) + assert.same("gopls", client.name) + end) + it("open virtual window", function() + end) +end) + +describe("UI", function () + nio.tests.it("open virtual window", function() end) end)