From 1fe20004ec05e494b0a24251ea603d72750d7a58 Mon Sep 17 00:00:00 2001 From: JuanZoran <1430359574@qq.com> Date: Mon, 13 Mar 2023 11:51:46 +0800 Subject: [PATCH] refactor: begin to refactor buffer node and buffer obj --- lua/Trans/core/conf.lua | 28 ++--- lua/Trans/frontend/hover/init.lua | 67 ++++++++++-- lua/Trans/init.lua | 8 +- lua/Trans/util/bing_node.lua | 166 ++++++++++++++++++++++++++++++ lua/Trans/util/node.lua | 11 +- lua/Trans/wrapper/buffer.lua | 120 ++++++++++++--------- lua/Trans/wrapper/window.lua | 11 +- 7 files changed, 326 insertions(+), 85 deletions(-) create mode 100644 lua/Trans/util/bing_node.lua diff --git a/lua/Trans/core/conf.lua b/lua/Trans/core/conf.lua index a694b53..a7ebf8f 100644 --- a/lua/Trans/core/conf.lua +++ b/lua/Trans/core/conf.lua @@ -32,9 +32,9 @@ return { title = title, -- need nvim-0.9 }, hover = { - width = 37, - height = 27, - keymap = { + width = 37, + height = 27, + keymap = { play = '_', pageup = '[[', pagedown = ']]', @@ -47,7 +47,7 @@ return { 'CursorMoved', 'BufLeave', }, - order = { + order = { 'title', 'tag', 'pos', @@ -55,21 +55,21 @@ return { 'translation', 'definition', }, - spinner = 'dots', -- see: /lua/Trans/style/spinner - fallback_message = '翻译超时或没有找到相关的翻译' -- TODO :support replace with {{special word}} + spinner = 'dots', -- see: /lua/Trans/style/spinner + fallback_message = '翻译超时或没有找到相关的翻译', -- TODO :support replace with {{special word}} + icon = { + -- or use emoji + star = '', -- ⭐ + notfound = ' ', -- ❔ + yes = '✔', -- ✔️ + no = '', -- ❌ + cell = '■', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █ + }, }, }, style = { -- see lua/Trans/style/theme.lua theme = 'default', -- default | tokyonight | dracula - -- or use emoji - icon = { - star = '', -- ⭐ - notfound = ' ', -- ❔ - yes = '✔', -- ✔️ - no = '', -- ❌ - cell = '■', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █ - }, }, } diff --git a/lua/Trans/frontend/hover/init.lua b/lua/Trans/frontend/hover/init.lua index ddfe755..b2fe01e 100644 --- a/lua/Trans/frontend/hover/init.lua +++ b/lua/Trans/frontend/hover/init.lua @@ -1,35 +1,70 @@ local Trans = require('Trans') -local M = Trans.metatable('frontend.hover') +---@class hover +---@field queue table @hover queue for all hover instances +---@field buffer buffer @buffer for hover window +---@field destroy_funcs table @functions to be executed when hover window is closed +---@field window window @hover window +---@field opts table @options for hover window +---@field opts.title string @title for hover window +---@field opts.width number @width for hover window +---@field opts.height number @height for hover window +---@field opts.animation boolean @whether to use animation for hover window +---@field opts.fallback_message string @message to be displayed when hover window is waiting for data +---@field opts.spinner string @spinner to be displayed when hover window is waiting for data +---@field opts.icon table @icons for hover window +---@field opts.icon.notfound string @icon for not found +---@field opts.icon.yes string @icon for yes +---@field opts.icon.no string @icon for no +---@field opts.icon.star string @icon for star +---@field opts.icon.cell string @icon for cell used in waitting animation -M.queue = {} +local M = Trans.metatable('frontend.hover', { + queue = {}, +}) M.__index = M - +---Create a new hover instance +---@return hover new_instance function M.new() local new_instance = { buffer = Trans.wrapper.buffer.new(), + destroy_funcs = {}, } M.queue[#M.queue + 1] = new_instance return setmetatable(new_instance, M) end +---Get the first active instances +---@return hover function M.get_active_instance() M.clear_dead_instance() return M.queue[1] end +---Clear dead instance function M.clear_dead_instance() - for i = #M.queue, 1, -1 do - if not M.queue[i]:is_available() then - --- FIXME :Del Buffer or ... ? - table.remove(M.queue, i) + local queue = M.queue + for i = #queue, 1, -1 do + if not queue[i]:is_available() then + queue[i]:destroy() + table.remove(queue, i) end end end +---Destroy hover instance and execute destroy functions +function M:destroy() + for _, func in ipairs(self.destroy_funcs) do + func(self) + end + + self.window:try_close() + self.buffer:destroy() +end + ---Init hover window ---@param opts table @window options: width, height ---@return unknown @@ -48,9 +83,9 @@ function M:init_window(opts) if win_opts.title then win_opts.title_pos = 'center' end - win_opts.width = win_opts.width or m_opts.width - win_opts.height = win_opts.height or m_opts.height - opts.animation = m_opts.animation + win_opts.width = win_opts.width or m_opts.width + win_opts.height = win_opts.height or m_opts.height + opts.animation = m_opts.animation @@ -58,14 +93,20 @@ function M:init_window(opts) return self.window end +---Wait for data +---@param tbl table @table to be checked +---@param name string @key to be checked +---@param timeout number @timeout for waiting function M:wait(tbl, name, timeout) local msg = self.opts.fallback_message local wid = msg:width() local spinner = Trans.style.spinner[self.opts.spinner] local size = #spinner + local cell = self.opts.icon.cell + local function update_text(times) - return spinner[times % size + 1] .. ('.'):rep(times) + return spinner[times % size + 1] .. (cell):rep(times) end self:init_window({ @@ -86,11 +127,15 @@ function M:wait(tbl, name, timeout) -- TODO : End waitting animation end +---Process data and display it in hover window +---@param data table @data to be processed function M:process(data) vim.pretty_print(data.result) print('TODO: process data') end +---Check if hover window and buffer are valid +---@return boolean @whether hover window and buffer are valid function M:is_available() return self.buffer:is_valid() and self.window:is_valid() end diff --git a/lua/Trans/init.lua b/lua/Trans/init.lua index ce74727..880eb40 100644 --- a/lua/Trans/init.lua +++ b/lua/Trans/init.lua @@ -1,5 +1,9 @@ -local function metatable(folder_name) - return setmetatable({}, { +---Set or Get metatable which will find module in folder +---@param folder_name string +---@param origin table? +---@return table +local function metatable(folder_name, origin) + return setmetatable(origin or {}, { __index = function(tbl, key) local status, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key)) diff --git a/lua/Trans/util/bing_node.lua b/lua/Trans/util/bing_node.lua new file mode 100644 index 0000000..7b749f7 --- /dev/null +++ b/lua/Trans/util/bing_node.lua @@ -0,0 +1,166 @@ +--- INFO : Generated by newbing + +-- 基类node +local Node = {} +Node.__index = Node + +-- 构造函数 +function Node:new(row, col, width, height) + local obj = { + row = row, + col = col, + width = width, + height = height, + } + setmetatable(obj, self) + return obj +end + +-- 渲染方法(空实现) +function Node:render() +end + +-- 更新方法(空实现) +function Node:update() +end + +-- 子类box node +local BoxNode = setmetatable({}, Node) +BoxNode.__index = BoxNode + +-- 构造函数 +function BoxNode:new(row, col, width, height, border_style) + local obj = Node.new(self, row, col, width, height) + obj.border_style = border_style or "single" + return obj +end + +-- 渲染方法(画边框) +function BoxNode:render() + local top_left_char = + self.border_style == "single" and "┌" or self.border_style == "double" and "╔" + local top_right_char = + self.border_style == "single" and "┐" or self.border_style == "double" and "╗" + local bottom_left_char = + self.border_style == "single" and "└" or self.border_style == "double" and "╚" + local bottom_right_char = + self.border_style == "single" and "┘" or self.border_style == "double" and "╝" + local horizontal_char = + self.border_style == "single" and "-" or self.border_style == "double" and "=" + local vertical_char = + self.border_style == "single" and "|" or self.border_style == "double" and "|" + + -- draw top line + vim.api.nvim_buf_set_text( + vim.api.nvim_get_current_buf(), + self.row, + self.col, + self.row, + math.min(self.col + self.width - 1), + { top_left_char .. horizontal_char:rep(self.width - 2) .. top_right_char } + ) + + -- draw bottom line + vim.api.nvim_buf_set_text( + vim.api.nvim_get_current_buf(), + math.min(self.row + self.height - 1), + math.max(self.col), + math.min(self.row + self.height - 1), + math.min(self.col + self.width - 1), + { bottom_left_char .. horizontal_char:rep(self.width - 2) .. bottom_right_char } + ) + + -- draw left line + for i = self.row + 1, self.row + self.height - 2 do + vim.api.nvim_buf_set_text( + vim.api.nvim_get_current_buf(), + i, + math.max(self.col), + i, + math.max(self.col + 1), + { vertical_char } + ) + end + + -- draw right line + for i = self.row + 1, self.row + self.height - 2 do + vim.api.nvim_buf_set_text( + vim.api.nvim_get_current_buf(), + i, + math.min(self.col + self.width - 1), + i, + math.min(self.col + self.width), + { vertical_char } + ) + end +end + +-- 更新方法(暂无) + +-- 子类text node +local TextNode = setmetatable({}, Node) +TextNode.__index = TextNode + +-- 构造函数 +function TextNode:new(row, col, width, height, text_content) + local obj = Node.new(self, row, col, width, height) + obj.text_content = text_content or "" + return obj +end + +-- 渲染方法(写入文本内容) +function TextNode:render() + -- split text content by newline character + local lines = vim.split(obj.text_content, "\n") + + -- write each line to buffer text within the node boundaries + for i, line in ipairs(lines) do + if i <= self.height then + vim.api.nvim_buf_set_text( + vim.api.nvim_get_current_buf(), + math.min(self.row + i - 1), math.max(self.col), + math.min(self.row + i - 1), + math.min(self.col + self.width - 1), + { line:sub(1, self.width) } + ) + end + end +end + +-- 更新方法(暂无) + +-- 子类extmark node +local ExtmarkNode = setmetatable({}, Node) +ExtmarkNode.__index = ExtmarkNode + +-- 构造函数 +function ExtmarkNode:new(row, col, width, height, hl_group) + local obj = Node.new(self, row, col, width, height) + obj.hl_group = hl_group or "Normal" + return obj +end + +-- 渲染方法(创建一个extmark) +function ExtmarkNode:render() + -- create a namespace for extmarks + local ns = vim.api.nvim_create_namespace("nodes") + + -- create an extmark with the given highlight group and position + vim.api.nvim_buf_set_extmark( + vim.api.nvim_get_current_buf(), + ns, + self.row, + self.col, + { hl_group = self.hl_group, end_line = self.row + self.height - 1, end_col = self.col + self.width - 1 } + ) +end + +-- 更新方法(暂无) + +-- 返回所有的节点类 +return { + Node = Node, + BoxNode = BoxNode, + TextNode = TextNode, + ExtmarkNode = ExtmarkNode, +} diff --git a/lua/Trans/util/node.lua b/lua/Trans/util/node.lua index 81729df..38a6d15 100644 --- a/lua/Trans/util/node.lua +++ b/lua/Trans/util/node.lua @@ -1,24 +1,23 @@ local api = vim.api -local ns = require('Trans').ns local add_hl = api.nvim_buf_add_highlight local item_meta = { - load = function(self, bufnr, line, col) + render = function(self, bufnr, line, col) if self[2] then - add_hl(bufnr, ns, self[2], line, col, col + #self[1]) + add_hl(bufnr, self.ns or -1, self[2], line, col, col + #self[1]) end end, } local text_meta = { - load = function(self, bufnr, line, col) + render = function(self, bufnr, line, col) local items = self.items local step = self.step or '' local len = #step for i = 1, self.size do local item = items[i] - item:load(bufnr, line, col) + item:render(bufnr, line, col) col = col + #item[1] + len end end @@ -41,7 +40,6 @@ return { [2] = highlight, }, item_meta) end, - text = function(items) local strs = {} local size = #items @@ -56,7 +54,6 @@ return { items = items, }, text_meta) end, - format = function(opts) local text = opts.text local size = text.size diff --git a/lua/Trans/wrapper/buffer.lua b/lua/Trans/wrapper/buffer.lua index a9c2027..92929aa 100644 --- a/lua/Trans/wrapper/buffer.lua +++ b/lua/Trans/wrapper/buffer.lua @@ -1,6 +1,5 @@ ---@class buf ---@field bufnr integer buffer handle ----@field size integer buffer line count local buffer = {} local api, fn = vim.api, vim.fn @@ -8,7 +7,6 @@ local api, fn = vim.api, vim.fn ---Clear all content in buffer function buffer:wipe() api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) - self.size = 0 end ---delete buffer [_start, _end] line content [one index] @@ -21,7 +19,6 @@ function buffer:del(_start, _end) _end = _end or _start fn.deletebufline(self.bufnr, _start, _end) end - self.size = api.nvim_buf_line_count(self.bufnr) end ---Set buffer option @@ -38,7 +35,8 @@ function buffer:option(name) return api.nvim_buf_get_option(self.bufnr, name) end -function buffer:delete() +---Destory buffer +function buffer:destory() api.nvim_buf_delete(self.bufnr, { force = true }) end @@ -76,57 +74,79 @@ function buffer:lines(i, j) return api.nvim_buf_get_lines(self.bufnr, i, j, false) end +---Add Extmark to buffer +---@param linenr number line number should be set[one index] +---@param col_start number column start +---@param col_end number column end +---@param hl_group string highlight group +---@param ns number? highlight namespace +function buffer:add_extmark(linenr, col_start, col_end, hl_group, ns) + linenr = linenr and linenr - 1 or -1 + api.nvim_buf_set_extmark(self.bufnr, ns or -1, linenr, col_start, { + end_line = linenr, + end_col = col_end, + hl_group = hl_group, + }) +end + +---Add highlight to buffer +---@param linenr number line number should be set[one index] +---@param col_start number column start +---@param col_end number column end +---@param hl_group string highlight group +---@param ns number? highlight namespace +function buffer:add_highlight(linenr, col_start, col_end, hl_group, ns) + linenr = linenr and linenr - 1 or -1 + api.nvim_buf_add_highlight(self.bufnr, ns or -1, hl_group, linenr, col_start, col_end) +end + ---Calculate buffer content display height ---@param width integer ---@return integer height function buffer:height(width) - local size = self.size local lines = self:lines() local height = 0 - for i = 1, size do - height = height + math.max(1, (math.ceil(lines[i]:width() / width))) + for _, line in ipairs(lines) do + height = height + math.max(1, (math.ceil(line:width() / width))) end return height end ----Add|Set line content +---Get buffer line count +---@return integer +function buffer:line_count() + return api.nvim_buf_line_count(self.bufnr) +end + +---Set line content ---@param nodes string|table|table[] string -> as line content | table -> as a node | table[] -> as node[] ----@param index number? line number should be set[one index] -function buffer:addline(nodes, index) - local newsize = self.size + 1 - assert(index == nil or index <= newsize) - index = index or newsize - if index == newsize then - self.size = newsize - end +---@param linenr number? line number should be set[one index] or let it be nil to append +function buffer:setline(nodes, linenr) + linenr = linenr and linenr - 1 or -1 if type(nodes) == 'string' then - self[index] = nodes - return - end + api.nvim_buf_set_lines(self.bufnr, linenr, linenr, false, { nodes }) + elseif type(nodes) == 'table' then + if type(nodes[1]) == 'string' then + -- FIXME :set [nodes] type as node + ---@diagnostic disable-next-line: assign-type-mismatch + api.nvim_buf_set_lines(self.bufnr, linenr, linenr, false, { nodes[1] }) + nodes:render(self, linenr, 0) + else + local strs = {} + local num = #nodes + for i = 1, num do + strs[i] = nodes[i][1] + end - - local line = index - 1 - local bufnr = self.bufnr - local col = 0 - if type(nodes[1]) == 'string' then - self[index] = nodes[1] - nodes:load(bufnr, line, col) - return - end - - - local strs = {} - local num = #nodes - for i = 1, num do - strs[i] = nodes[i][1] - end - - self[index] = table.concat(strs) - for i = 1, num do - local node = nodes[i] - node:load(bufnr, line, col) - col = col + #node[1] + api.nvim_buf_set_lines(self.bufnr, linenr, linenr, false, { table.concat(strs) }) + local col = 0 + for i = 1, num do + local node = nodes[i] + node:render(self, linenr, col) + col = col + #node[1] + end + end end end @@ -136,28 +156,32 @@ buffer.__index = function(self, key) if res then return res elseif type(key) == 'number' then - return fn.getbufoneline(self.bufnr, key) + -- return fn.getbufoneline(self.bufnr, key) -- Vimscript Function Or Lua API ?? + return api.nvim_buf_get_lines(self.bufnr, key - 1, key, true)[1] else error('invalid key' .. key) end end + ---@private -buffer.__newindex = function(self, key, text) - assert(key <= self.size + 1) - fn.setbufline(self.bufnr, key, text) +buffer.__newindex = function(self, key, nodes) + if type(key) == 'number' then + self:setline(nodes, key) + else + rawset(self, key, nodes) + end end ---buffer constructor ---@return buf function buffer.new() local new_buf = setmetatable({ - bufnr = -1, - size = 0, + bufnr = api.nvim_create_buf(false, false), + extmarks = {}, }, buffer) - new_buf.bufnr = api.nvim_create_buf(false, false) new_buf:set('filetype', 'Trans') new_buf:set('buftype', 'nofile') return new_buf diff --git a/lua/Trans/wrapper/window.lua b/lua/Trans/wrapper/window.lua index 778022f..9fa6bff 100644 --- a/lua/Trans/wrapper/window.lua +++ b/lua/Trans/wrapper/window.lua @@ -3,7 +3,7 @@ local Trans = require("Trans") ---@class win ----@field win_opts table window config [**when open**] +---@field win_opts table window config [**When open**] ---@field winid integer window handle ---@field ns integer namespace for highlight ---@field animation table window animation @@ -49,7 +49,7 @@ function window:set_width(width) api.nvim_win_set_width(self.winid, width) end ----Get window width +---Get window width function window:width() return api.nvim_win_get_width(self.winid) end @@ -87,7 +87,7 @@ function window:smooth_expand(opts) self:set('wrap', wrap) end ----Close window +---Try to close window with animation? function window:try_close() local close_animation = self.animation.close if close_animation then @@ -112,6 +112,7 @@ function window:set_hl(name, opts) api.nvim_set_hl(self.ns, name, opts) end +---Open window with animation? function window:open() local win_opts = self.win_opts local open_animation = self.animation.open @@ -159,6 +160,10 @@ local default_opts = { }, } + +---Create new window +---@param opts table window config +---@return win function window.new(opts) opts = vim.tbl_deep_extend('keep', opts, default_opts)