diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9b4c23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Kai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bcd2ef5..e31abd7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,5 @@ # Java Projects -- 使用 [symbols-outline](https://github.com/simrat39/symbols-outline.nvim) 代码实现预览 -- [vscode-java-dependency](https://github.com/Microsoft/vscode-java-dependency) 提供数据支持 - -![java-deps](https://javahello.github.io/dev/nvim-lean/images/java-deps.png) - ## 安装 [English](https://github.com/JavaHello/java-deps.nvim/issues/2) @@ -18,13 +13,51 @@ ft = "java", dependencies = "mfussenegger/nvim-jdtls", config = function() - require("java-deps").setup({}) + require("java-deps").setup({ + symbols = { + icons = { + NodeKind = { + -- project 节点改成文件夹图标 + Project = "󰉋", + -- 也支持同时覆盖 icon 和高亮 + Workspace = { icon = "󱁐", hl = "Directory" }, + }, + TypeKind = { + Class = "󰌗", + Interface = { icon = "", hl = "Function" }, + }, + }, + highlights = { + default_icon = "Identifier", + NodeKind = { + File = "Directory", + Package = "Directory", + }, + }, + }, + highlights = { + LineGuide = { link = "Comment" }, + }, + }) end, } ``` -- 手动编译 `vscode-java-dependency` +可配置项说明: + +- `symbols.icons.<分类>.<枚举名>`: 覆盖图标,值可以是字符串,或者 `{ icon = "...", hl = "..." }` +- `symbols.highlights.default_icon`: 没有单独指定时,图标默认使用的高亮组 +- `symbols.highlights.<分类>.<枚举名>`: 仅覆盖某个图标的高亮组 +- `highlights.LineGuide`: 定义 `JavaDepsLineGuide` 高亮组 + +目前支持的分类: + +- `symbols.icons.NodeKind`: `Workspace` `Project` `PackageRoot` `Package` `PrimaryType` `CompilationUnit` `ClassFile` `Container` `Folder` `File` +- `symbols.icons.TypeKind`: `Class` `Interface` `Enum` +- `symbols.icons.EntryKind`: `K_SOURCE` `K_BINARY` + +- 手动编译 `vscode-java-dependency` (可选) ```sh git clone https://github.com/microsoft/vscode-java-dependency.git @@ -33,7 +66,7 @@ npm install npm run build-server ``` -- 将 `vscode-java-dependency` 编译后的 `jar` 添加到 jdtls_config["init_options"].bundles 中 +- 将 `vscode-java-dependency` 的 `jar` 包添加到 jdtls_config["init_options"].bundles 中 ```lua local jdtls_config = {} @@ -56,11 +89,10 @@ jdtls_config["init_options"] = { } ``` -- 添加 attach +- 添加命令 ```lua jdtls_config["on_attach"] = function(client, buffer) - require("java-deps").attach(client, buffer) -- 添加命令 local create_command = vim.api.nvim_buf_create_user_command create_command(buffer, "JavaProjects", require("java-deps").toggle_outline, { @@ -76,3 +108,8 @@ end :lua require('java-deps').open_outline() :lua require('java-deps').close_outline() ``` + +## 参考实现 + +- 使用 [symbols-outline](https://github.com/simrat39/symbols-outline.nvim) 代码实现预览 +- [vscode-java-dependency](https://github.com/Microsoft/vscode-java-dependency) 提供数据支持 diff --git a/lua/java-deps/config.lua b/lua/java-deps/config.lua index 698f341..989ecbc 100644 --- a/lua/java-deps/config.lua +++ b/lua/java-deps/config.lua @@ -3,7 +3,7 @@ local M = { options = { show_guides = true, auto_close = false, - width = 40, + width = "30%", show_numbers = false, show_relative_numbers = false, preview_bg_highlight = "Pmenu", @@ -17,13 +17,44 @@ local M = { toggle_fold = "o", }, symbols = { - icons = {}, + icons = { + NodeKind = {}, + TypeKind = {}, + EntryKind = {}, + }, + highlights = { + default_icon = "Type", + NodeKind = {}, + TypeKind = {}, + EntryKind = {}, + }, + }, + highlights = { + LineGuide = { link = "Comment" }, }, }, } M.setup = function(config) if config then - M = vim.tbl_extend("force", M, config) + local normalized = vim.deepcopy(config) + local option_keys = vim.tbl_keys(M.options) + + for _, key in ipairs(option_keys) do + if normalized[key] ~= nil then + normalized.options = normalized.options or {} + if type(normalized[key]) == "table" then + normalized.options[key] = vim.tbl_deep_extend("force", normalized.options[key] or {}, normalized[key]) + else + normalized.options[key] = normalized[key] + end + normalized[key] = nil + end + end + + local new_config = vim.tbl_deep_extend("force", M, normalized) + for key, value in pairs(new_config) do + M[key] = value + end end end @@ -44,6 +75,16 @@ function M.get_split_command() end end function M.get_window_width() - return M.options.width + local width = M.options.width + if type(width) == "string" then + local percent = tonumber(width:match("^%s*(%d+)%%%s*$")) + if percent ~= nil then + return math.max(1, math.floor(vim.o.columns * percent / 100)) + end + end + if type(width) == "number" then + return math.max(1, math.floor(width)) + end + return math.max(1, math.floor(vim.o.columns * 0.3)) end return M diff --git a/lua/java-deps/highlight.lua b/lua/java-deps/highlight.lua index ce2af2a..bf8adf2 100644 --- a/lua/java-deps/highlight.lua +++ b/lua/java-deps/highlight.lua @@ -1,33 +1,31 @@ +local config = require("java-deps.config") + local M = { items = { nsid = vim.api.nvim_create_namespace("java-deps-items"), - highlights = { - LineGuide = { link = "Comment" }, - }, }, } M.init_hl = function() - local ihlf = function(hls) - for name, hl in pairs(hls.highlights) do - if vim.fn.hlexists("JavaDeps" .. name) == 0 then - vim.api.nvim_set_hl(0, "JavaDeps" .. name, { link = hl.link }) - end - end + local highlights = config.options.highlights or {} + for name, hl in pairs(highlights) do + vim.api.nvim_set_hl(0, "JavaDeps" .. name, hl) end - ihlf(M.items) end M.clear_all_ns = function(bufnr) - vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1) + vim.api.nvim_buf_clear_namespace(bufnr, M.items.nsid, 0, -1) end ---@param bufnr number ---@param hl_info table ----@param nodes TreeItem[] -function M.add_item_highlights(bufnr, hl_info, nodes) +---@param _ TreeItem[] +function M.add_item_highlights(bufnr, hl_info, _) for _, line_hl in ipairs(hl_info) do local line, hl_start, hl_end, hl_type = unpack(line_hl) - vim.api.nvim_buf_add_highlight(bufnr, M.items.nsid, hl_type, line - 1, hl_start, hl_end) + vim.api.nvim_buf_set_extmark(bufnr, M.items.nsid, line - 1, hl_start, { + end_col = hl_end, + hl_group = hl_type, + }) end end diff --git a/lua/java-deps/java/lsp-command.lua b/lua/java-deps/java/lsp-command.lua index 653cf3c..97f2800 100644 --- a/lua/java-deps/java/lsp-command.lua +++ b/lua/java-deps/java/lsp-command.lua @@ -33,7 +33,7 @@ M.execute_command_async = function(command, callback, bufnr) end end end - client.request("workspace/executeCommand", command, callback, bufnr) + client:request("workspace/executeCommand", command, callback, bufnr) if co then return coroutine.yield() end @@ -43,7 +43,7 @@ M.execute_command = function(command, bufnr) if not client then return end - local resp = client.request_sync("workspace/executeCommand", command, 20000, bufnr) + local resp = client:request_sync("workspace/executeCommand", command, 20000, bufnr) if not resp then return "No response" end diff --git a/lua/java-deps/parser.lua b/lua/java-deps/parser.lua index 5b03c1a..6a45807 100644 --- a/lua/java-deps/parser.lua +++ b/lua/java-deps/parser.lua @@ -18,6 +18,20 @@ local function table_to_str(t) return ret end +---@param line_number integer +---@param parts string[] +---@param hl_info table +local function add_prefix_highlights(line_number, parts, hl_info) + local col = 0 + for _, part in ipairs(parts) do + local start_col, end_col = part:find("%S+") + if start_col ~= nil and end_col ~= nil then + table.insert(hl_info, { line_number, col + start_col - 1, col + end_col, "JavaDepsLineGuide" }) + end + col = col + #part + end +end + local guides = { markers = { bottom = "└", @@ -66,11 +80,8 @@ function M.get_lines(flattened_outline_items) line[index] = line[index] .. " " end - local string_prefix = "" - - for _, value in ipairs(line) do - string_prefix = string_prefix .. tostring(value) - end + local string_prefix = table_to_str(line) + add_prefix_highlights(node_line, line, hl_info) local hl_icon = icons.get_icon(node.data) local icon = hl_icon.icon @@ -78,7 +89,7 @@ function M.get_lines(flattened_outline_items) local hl_start = #string_prefix local hl_end = #string_prefix + #icon - local hl_type = hl_icon.hl or "Type" + local hl_type = hl_icon.hl or config.options.symbols.highlights.default_icon or "Type" table.insert(hl_info, { node_line, hl_start, hl_end, hl_type }) node.prefix_length = #string_prefix + #icon + 1 end diff --git a/lua/java-deps/preview.lua b/lua/java-deps/preview.lua index 3b08731..7a8e5b8 100644 --- a/lua/java-deps/preview.lua +++ b/lua/java-deps/preview.lua @@ -73,7 +73,7 @@ end local function setup_preview_buf() local code_buf = vim.api.nvim_win_get_buf(jd.state.code_win) - local ft = vim.api.nvim_buf_get_option(code_buf, "filetype") + local ft = vim.api.nvim_get_option_value("filetype", { buf = code_buf }) local function treesitter_attach() local ts_highlight = require("nvim-treesitter.highlight") @@ -84,9 +84,9 @@ local function setup_preview_buf() -- user might not have tree sitter installed pcall(treesitter_attach) - vim.api.nvim_buf_set_option(state.preview_buf, "syntax", ft) - vim.api.nvim_buf_set_option(state.preview_buf, "bufhidden", "delete") - vim.api.nvim_win_set_option(state.preview_win, "cursorline", true) + vim.api.nvim_set_option_value("syntax", ft, { buf = state.preview_buf }) + vim.api.nvim_set_option_value("bufhidden", "delete", { buf = state.preview_buf }) + vim.api.nvim_set_option_value("cursorline", true, { win = state.preview_win }) update_preview(code_buf) end @@ -138,13 +138,11 @@ local function update_hover() { kind = "markdown", value = "[" .. node.name .. "](" .. node.path .. ")" }, } local markdown_lines = vim.lsp.util.convert_input_to_markdown_lines(mdstring) - markdown_lines = vim.lsp.util.trim_empty_lines(markdown_lines) + markdown_lines = vim.split(table.concat(markdown_lines, "\n"), "\n", { plain = true, trimempty = true }) if vim.tbl_isempty(markdown_lines) then markdown_lines = { "###No info available!" } end - markdown_lines = vim.lsp.util.stylize_markdown(state.hover_buf, markdown_lines, {}) - if state.hover_buf ~= nil then vim.api.nvim_buf_set_lines(state.hover_buf, 0, -1, 0, markdown_lines) end @@ -155,21 +153,24 @@ local function setup_hover_buf() return end -- local code_buf = vim.api.nvim_win_get_buf(jd.state.code_win) - -- local ft = vim.api.nvim_buf_get_option(code_buf, "filetype") - -- vim.api.nvim_buf_set_option(state.hover_buf, "syntax", "xml") - vim.api.nvim_buf_set_option(state.hover_buf, "bufhidden", "delete") - vim.api.nvim_win_set_option(state.hover_win, "wrap", true) - vim.api.nvim_win_set_option(state.hover_win, "cursorline", false) + -- local ft = vim.api.nvim_get_option_value("filetype", { buf = code_buf }) + -- vim.api.nvim_set_option_value("syntax", "xml", { buf = state.hover_buf }) + vim.api.nvim_set_option_value("filetype", "markdown", { buf = state.hover_buf }) + vim.api.nvim_set_option_value("bufhidden", "delete", { buf = state.hover_buf }) + vim.api.nvim_set_option_value("wrap", true, { win = state.hover_win }) + vim.api.nvim_set_option_value("conceallevel", 2, { win = state.hover_win }) + vim.api.nvim_set_option_value("cursorline", false, { win = state.hover_win }) + pcall(vim.treesitter.start, state.hover_buf, "markdown") update_hover() end local function set_bg_hl() local winhi = "Normal:" .. config.options.preview_bg_highlight - -- vim.api.nvim_win_set_option(state.preview_win, "winhighlight", winhi) - vim.api.nvim_win_set_option(state.hover_win, "winhighlight", winhi) + -- vim.api.nvim_set_option_value("winhighlight", winhi, { win = state.preview_win }) + vim.api.nvim_set_option_value("winhighlight", winhi, { win = state.hover_win }) local winblend = config.options.winblend - -- vim.api.nvim_win_set_option(state.preview_win, "winblend", winblend) - vim.api.nvim_win_set_option(state.hover_win, "winblend", winblend) + -- vim.api.nvim_set_option_value("winblend", winblend, { win = state.preview_win }) + vim.api.nvim_set_option_value("winblend", winblend, { win = state.hover_win }) end diff --git a/lua/java-deps/view.lua b/lua/java-deps/view.lua index 8d88081..0f2d58c 100644 --- a/lua/java-deps/view.lua +++ b/lua/java-deps/view.lua @@ -25,7 +25,7 @@ function View:setup_view() self.bufnr = vim.api.nvim_create_buf(false, true) -- delete buffer when window is closed / buffer is hidden - vim.api.nvim_buf_set_option(self.bufnr, "bufhidden", "delete") + vim.api.nvim_set_option_value("bufhidden", "delete", { buf = self.bufnr }) -- create a split vim.cmd(config.get_split_command()) -- resize to a % of the current window size @@ -36,32 +36,32 @@ function View:setup_view() vim.api.nvim_win_set_buf(self.winnr, self.bufnr) -- window stuff - vim.api.nvim_win_set_option(self.winnr, "spell", false) - vim.api.nvim_win_set_option(self.winnr, "signcolumn", "no") - vim.api.nvim_win_set_option(self.winnr, "foldcolumn", "0") - vim.api.nvim_win_set_option(self.winnr, "number", false) - vim.api.nvim_win_set_option(self.winnr, "relativenumber", false) - vim.api.nvim_win_set_option(self.winnr, "winfixwidth", true) - vim.api.nvim_win_set_option(self.winnr, "list", false) - vim.api.nvim_win_set_option(self.winnr, "wrap", config.options.wrap) - vim.api.nvim_win_set_option(self.winnr, "linebreak", true) -- only has effect when wrap=true - vim.api.nvim_win_set_option(self.winnr, "breakindent", true) -- only has effect when wrap=true + vim.api.nvim_set_option_value("spell", false, { win = self.winnr }) + vim.api.nvim_set_option_value("signcolumn", "no", { win = self.winnr }) + vim.api.nvim_set_option_value("foldcolumn", "0", { win = self.winnr }) + vim.api.nvim_set_option_value("number", false, { win = self.winnr }) + vim.api.nvim_set_option_value("relativenumber", false, { win = self.winnr }) + vim.api.nvim_set_option_value("winfixwidth", true, { win = self.winnr }) + vim.api.nvim_set_option_value("list", false, { win = self.winnr }) + vim.api.nvim_set_option_value("wrap", config.options.wrap, { win = self.winnr }) + vim.api.nvim_set_option_value("linebreak", true, { win = self.winnr }) -- only has effect when wrap=true + vim.api.nvim_set_option_value("breakindent", true, { win = self.winnr }) -- only has effect when wrap=true -- Would be nice to use ui.markers.vertical as part of showbreak to keep -- continuity of the tree UI, but there's currently no way to style the -- color, apart from globally overriding hl-NonText, which will potentially -- mess with other theme/user settings. So just use empty spaces for now. - vim.api.nvim_win_set_option(self.winnr, "showbreak", " ") -- only has effect when wrap=true. + vim.api.nvim_set_option_value("showbreak", " ", { win = self.winnr }) -- only has effect when wrap=true. -- buffer stuff vim.api.nvim_buf_set_name(self.bufnr, "JavaProjects") - vim.api.nvim_buf_set_option(self.bufnr, "filetype", "JavaProjects") - vim.api.nvim_buf_set_option(self.bufnr, "modifiable", false) + vim.api.nvim_set_option_value("filetype", "JavaProjects", { buf = self.bufnr }) + vim.api.nvim_set_option_value("modifiable", false, { buf = self.bufnr }) if config.options.show_numbers or config.options.show_relative_numbers then - vim.api.nvim_win_set_option(self.winnr, "nu", true) + vim.api.nvim_set_option_value("nu", true, { win = self.winnr }) end if config.options.show_relative_numbers then - vim.api.nvim_win_set_option(self.winnr, "rnu", true) + vim.api.nvim_set_option_value("rnu", true, { win = self.winnr }) end end diff --git a/lua/java-deps/views/icons.lua b/lua/java-deps/views/icons.lua index 706b217..6d29fce 100644 --- a/lua/java-deps/views/icons.lua +++ b/lua/java-deps/views/icons.lua @@ -1,23 +1,25 @@ +local config = require("java-deps.config") local node_data = require("java-deps.java.nodeData") local PackageRootKind = require("java-deps.java.IPackageRootNodeData").PackageRootKind local NodeKind = node_data.NodeKind local TypeKind = node_data.TypeKind +local has_devicons, devicons = pcall(require, "nvim-web-devicons") ---@class Icon ---@field icon string ---@field hl string? -local M = { +local defaults = { NodeKind = { [NodeKind.Workspace] = { icon = "", hl = "Type" }, [NodeKind.Project] = { icon = "" }, - [NodeKind.PackageRoot] = { icon = "" }, - [NodeKind.Package] = { icon = "" }, + [NodeKind.PackageRoot] = { icon = "" }, + [NodeKind.Package] = { icon = "" }, [NodeKind.PrimaryType] = { icon = "󰠱" }, [NodeKind.CompilationUnit] = { icon = "" }, [NodeKind.ClassFile] = { icon = "" }, [NodeKind.Container] = { icon = "" }, - [NodeKind.Folder] = { icon = "󰉋" }, + [NodeKind.Folder] = { icon = "" }, [NodeKind.File] = { icon = "󰈙" }, }, TypeKind = { @@ -26,20 +28,134 @@ local M = { [TypeKind.Enum] = { icon = "" }, }, EntryKind = { - [PackageRootKind.K_SOURCE] = { icon = "" }, - [PackageRootKind.K_BINARY] = { icon = "" }, + [PackageRootKind.K_SOURCE] = { icon = "" }, + [PackageRootKind.K_BINARY] = { icon = "" }, }, } +local M = {} +local enum_names = { + NodeKind = NodeKind, + TypeKind = TypeKind, + EntryKind = PackageRootKind, +} + +local file_like_extensions = { + [NodeKind.PrimaryType] = "java", + [NodeKind.CompilationUnit] = "java", + [NodeKind.ClassFile] = "class", +} + +local file_like_kinds = { + [NodeKind.PrimaryType] = true, + [NodeKind.CompilationUnit] = true, + [NodeKind.ClassFile] = true, + [NodeKind.File] = true, +} + +---@param value string|Icon|nil +---@return Icon +local function normalize_icon(value) + if type(value) == "string" then + return { icon = value } + end + if type(value) == "table" then + return vim.deepcopy(value) + end + return {} +end + +---@param category "NodeKind"|"TypeKind"|"EntryKind" +---@param overrides table +---@param key integer|string +local function get_override_value(category, overrides, key) + if overrides[key] ~= nil then + return overrides[key] + end + + for name, value in pairs(enum_names[category]) do + if value == key then + return overrides[name] + end + end +end + +---@param node DataNode +---@return Icon +local function get_devicon_icon(node) + if not has_devicons then + return {} + end + + local kind = node:kind() + if not file_like_kinds[kind] then + return {} + end + + local name = node._nodeData:getName() + local path = node._nodeData:getPath() + local filename = path or name + local extension = file_like_extensions[kind] + + if type(filename) ~= "string" or filename == "" then + return {} + end + + local basename = vim.fs.basename(filename) + + if extension == nil then + extension = basename:match("%.([^./\\]+)$") + end + + local icon, hl = devicons.get_icon(basename, extension, { default = false }) + if icon == nil and hl == nil then + return {} + end + return { + icon = icon, + hl = hl, + } +end + +---@param category "NodeKind"|"TypeKind"|"EntryKind" +---@param key integer|string +---@param node? DataNode +---@return Icon +local function resolve_icon(category, key, node) + local symbol_config = config.options.symbols or {} + local icon_overrides = (symbol_config.icons or {})[category] or {} + local highlight_overrides = (symbol_config.highlights or {})[category] or {} + local devicon = node ~= nil and get_devicon_icon(node) or {} + + local icon = vim.tbl_deep_extend( + "force", + defaults[category][key] or {}, + devicon, + normalize_icon(get_override_value(category, icon_overrides, key)) + ) + local hl_override = get_override_value(category, highlight_overrides, key) + if hl_override ~= nil then + icon.hl = hl_override + end + + return icon +end + ---@param node DataNode ---@return Icon M.get_icon = function(node) local kind = node:kind() if kind == node_data.NodeKind.PrimaryType then - return M.TypeKind[node:typeKind()] - else - return M.NodeKind[kind] + return resolve_icon("TypeKind", node:typeKind(), node) + end + if kind == node_data.NodeKind.PackageRoot and node._nodeData.getEntryKind then + local entry_kind = node._nodeData:getEntryKind() + local entry_icon = resolve_icon("EntryKind", entry_kind, node) + if entry_icon.icon ~= nil then + return entry_icon + end end + return resolve_icon("NodeKind", kind, node) end return M diff --git a/lua/java-deps/writer.lua b/lua/java-deps/writer.lua index ce12ec7..f78ac28 100644 --- a/lua/java-deps/writer.lua +++ b/lua/java-deps/writer.lua @@ -6,7 +6,7 @@ local M = {} local function is_buffer_outline(bufnr) local isValid = vim.api.nvim_buf_is_valid(bufnr) local name = vim.api.nvim_buf_get_name(bufnr) - local ft = vim.api.nvim_buf_get_option(bufnr, "filetype") + local ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) return string.match(name, "JavaProjects") ~= nil and ft == "JavaProjects" and isValid end @@ -14,9 +14,9 @@ function M.write_outline(bufnr, lines) if not is_buffer_outline(bufnr) then return end - vim.api.nvim_buf_set_option(bufnr, "modifiable", true) + vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr }) end ---@param bufnr integer