From d8df2abe655ec2468bf0f6ab898dd909f8c73479 Mon Sep 17 00:00:00 2001 From: Fasamii Date: Thu, 5 Feb 2026 19:25:43 +0100 Subject: rename --- README.md | 48 ++++++++++++ lua/netrw-icons/init.lua | 167 +++++++++++++++++++++++++++++++++++++++ lua/netrw-icons/parse.lua | 193 ++++++++++++++++++++++++++++++++++++++++++++++ lua/netrw/init.lua | 149 ----------------------------------- lua/netrw/parse.lua | 185 -------------------------------------------- 5 files changed, 408 insertions(+), 334 deletions(-) create mode 100644 README.md create mode 100644 lua/netrw-icons/init.lua create mode 100644 lua/netrw-icons/parse.lua delete mode 100644 lua/netrw/init.lua delete mode 100644 lua/netrw/parse.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbbdb2c --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# netrw-icons.nvim + +An extension plugin for Neovim’s built-in `netrw` file explorer that adds: + +- File and directory icons +- LSP diagnostics indicators + +## Features + +- No replacement of `netrw` — pure extension +- File and directory icons inside `netrw` +- Supports multiple icon providers: + - `nvim-web-devicons` + - `mini.icons` +- LSP diagnostics integration + +## Requirements + +- Neovim ≥ 0.9 +- One of the following (optional): + - [`nvim-web-devicons`](https://github.com/nvim-tree/nvim-web-devicons) + - [`mini.icons`](https://github.com/echasnovski/mini.nvim) +- Neovim built-in LSP (optional, for diagnostics) + +## Installation + +Using `lazy.nvim`: + +```lua +{ + "yourname/netrw-plus.nvim", + dependencies = { + "nvim-tree/nvim-web-devicons", -- or "echasnovski/mini.nvim", + }, + config = function() + require("netrw-plus").setup({ + prefer = "devicons" + file = true, + dir = true, + lsp = { + info = false, + hint = false, + warn = true, + error = true, + } + }) + end, +} diff --git a/lua/netrw-icons/init.lua b/lua/netrw-icons/init.lua new file mode 100644 index 0000000..608d479 --- /dev/null +++ b/lua/netrw-icons/init.lua @@ -0,0 +1,167 @@ +-- SPDX-License-Identifier: MIT +-- +-- Copyright (c) 2026 Mikołaj Kozłowski +-- +-- Portions of this file are derived from: +-- prichrd/netrw.nvim (MIT) +-- https://github.com/prichrd/netrw.nvim + +local M = {} +local parse = require("netrw.parse") + +local icon_provider = nil + +local function get_icon_provider(prefer) + local providers = {} + + local has_devicons, devicons = pcall(require, "nvim-web-devicons") + if has_devicons then + providers.devicons = devicons + end + + local has_miniicons, miniicons = pcall(require, "mini.icons") + if has_miniicons then + providers.miniicons = miniicons + end + + if prefer and providers[prefer] then + return { type = prefer, provider = providers[prefer] } + end + + if providers.miniicons then + return { type = "miniicons", provider = providers.miniicons } + elseif providers.devicons then + return { type = "devicons", provider = providers.devicons } + end + + return nil +end + +local function get_file_icon(node) + if not icon_provider then + return nil, nil + end + + if icon_provider.type == "devicons" then + local icon, hl = icon_provider.provider.get_icon(node.node, nil, { strict = true, default = false }) + return icon, hl + elseif icon_provider.type == "miniicons" then + local icon, hl = icon_provider.provider.get("file", node.node) + return icon, hl + end + + return nil, nil +end + +local function get_lsp_diagnostics(bufnr, node) + local diagnostics = {} + local filepath = node.dir .. "/" .. node.node + + local diag = vim.diagnostic.get(nil, { path = filepath }) + + for _, d in ipairs(diag) do + if d.severity == vim.diagnostic.severity.ERROR and M.options.lsp.error then + table.insert(diagnostics, { icon = "E", hl = "DiagnosticError" }) + elseif d.severity == vim.diagnostic.severity.WARN and M.options.lsp.warn then + table.insert(diagnostics, { icon = "W", hl = "DiagnosticWarn" }) + elseif d.severity == vim.diagnostic.severity.INFO and M.options.lsp.info then + table.insert(diagnostics, { icon = "I", hl = "DiagnosticInfo" }) + elseif d.severity == vim.diagnostic.severity.HINT and M.options.lsp.hint then + table.insert(diagnostics, { icon = "H", hl = "DiagnosticHint" }) + end + end + + return diagnostics +end + +local function draw(bufnr) + local namespace = vim.api.nvim_create_namespace("netrw") + vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) + + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + for i, line in ipairs(lines) do + local node = parse.get_node(line) + if node then + -- Draw file/dir icon on the left + if (node.type == parse.TYPE_FILE and M.options.file) or + (node.type == parse.TYPE_DIR and M.options.dir) then + local icon, hl = get_file_icon(node) + if icon then + local opts = { + id = i * 2, -- Unique ID for icon + virt_text = hl and { { icon .. " ", hl } } or { { icon .. " " } }, + virt_text_pos = "inline", + } + vim.api.nvim_buf_set_extmark(bufnr, namespace, i - 1, node.icon_col, opts) + end + end + + -- Draw LSP diagnostics on the right + if node.type == parse.TYPE_FILE then + local diagnostics = get_lsp_diagnostics(bufnr, node) + if #diagnostics > 0 then + local virt_text = {} + for _, diag in ipairs(diagnostics) do + table.insert(virt_text, { " " .. diag.icon, diag.hl }) + end + + local opts = { + id = i * 2 + 1, -- Unique ID for LSP + virt_text = virt_text, + virt_text_pos = "inline", + } + vim.api.nvim_buf_set_extmark(bufnr, namespace, i - 1, node.lsp_col, opts) + end + end + end + end +end + +--- @class Config +M.options = {} + +--- @class Config +local default = { + -- Preferred icon provider: + -- nil -> auto-detect + -- "miniicons" -> use mini.icons + -- "devicons" -> use nvim-web-devicons + prefer = nil, + + -- Show icons for files + file = true, + + -- Show icons for directories + dir = true, + + -- LSP diagnostics integration + lsp = { + info = false, + hint = false, + warn = true, + error = true, + } +} + +---@param options Config|nil +function M.setup(options) + M.options = vim.tbl_deep_extend("force", {}, default, options or {}) + + if M.options.file or M.options.dir then + icon_provider = get_icon_provider(M.options.prefer) + end + + vim.api.nvim_create_autocmd({ "BufEnter", "DiagnosticChanged" }, { + pattern = "*", + group = vim.api.nvim_create_augroup("netrw_icons", { clear = true }), + callback = function(args) + if vim.bo[args.buf].filetype ~= "netrw" then + return + end + draw(args.buf) + end + }) +end + +return M diff --git a/lua/netrw-icons/parse.lua b/lua/netrw-icons/parse.lua new file mode 100644 index 0000000..61460a0 --- /dev/null +++ b/lua/netrw-icons/parse.lua @@ -0,0 +1,193 @@ +-- SPDX-License-Identifier: MIT +-- +-- Copyright (c) 2026 Mikołaj Kozłowski +-- +-- Portions of this file are derived from: +-- prichrd/netrw.nvim (MIT) +-- https://github.com/prichrd/netrw.nvim + +local M = {} + +M.TYPE_DIR = 0 +M.TYPE_FILE = 1 +M.TYPE_SYMLINK = 2 + +---@alias Word {dir:string, node:string, link:string|nil, extension:string|nil, type:number, icon_col:number, name_col:number, lsp_col:number} + +---@param line string +---@param curdir string +---@return Word|nil +local parse_liststyle_0 = function(line, curdir) + local _, _, node, link = string.find(line, "^(.+)@\t%s*%-%->%s*(.+)") + if node then + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #node, + node = node, + extension = vim.fn.fnamemodify(node, ":e"), + link = link, + type = M.TYPE_SYMLINK, + } + end + + local _, _, dir = string.find(line, "^(.*)/") + if dir then + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #dir + 1, -- +1 for the "/" + node = dir, + type = M.TYPE_DIR, + } + end + + local ext = vim.fn.fnamemodify(line, ":e") + local clean_line = line + if string.sub(ext, -1) == "*" then + ext = string.sub(ext, 1, -2) + clean_line = string.sub(line, 1, -2) + end + + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #clean_line, + node = clean_line, + extension = ext, + type = M.TYPE_FILE, + } +end + +---@param line string +---@param curdir string +---@return Word|nil +local parse_liststyle_1 = function(line, curdir) + local _, _, node, link = string.find(line, "^(.+)@%s+") + if node then + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #node, + node = node, + extension = vim.fn.fnamemodify(node, ":e"), + link = link, + type = M.TYPE_SYMLINK, + } + end + + local _, _, dir = string.find(line, "^(.*)/") + if dir then + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #dir + 1, + node = dir, + type = M.TYPE_DIR, + } + end + + local file = vim.fn.substitute(line, "^\\(\\%(\\S\\+ \\)*\\S\\+\\).\\{-}$", "\\1", "e") + local ext = vim.fn.fnamemodify(file, ":e") + if string.sub(ext, -1) == "*" then + ext = string.sub(ext, 1, -2) + file = string.sub(file, 1, -2) + end + + return { + dir = curdir, + icon_col = 0, + name_col = 0, + lsp_col = #file, + node = file, + extension = ext, + type = M.TYPE_FILE, + } +end + +---@param line string +---@return Word|nil +local parse_liststyle_3 = function(line) + local curdir = vim.b.netrw_curdir + local _, to = string.find(line, "^[|%s]*") + local pipelessLine = string.sub(line, to + 1, #line) + + if pipelessLine == "" then + return nil + end + + local _, _, node, link = string.find(pipelessLine, "^(.+)@\t%s*%-%->%s*(.+)") + if node then + return { + dir = curdir, + icon_col = to, + name_col = to, + lsp_col = to + #node, + node = node, + extension = vim.fn.fnamemodify(node, ":e"), + link = link, + type = M.TYPE_SYMLINK, + } + end + + local _, _, dir = string.find(pipelessLine, "^(.*)/") + if dir then + return { + dir = curdir, + icon_col = to, + name_col = to, + lsp_col = to + #dir + 1, + node = dir, + type = M.TYPE_DIR, + } + end + + local ext = vim.fn.fnamemodify(pipelessLine, ":e") + local clean_line = pipelessLine + if string.sub(ext, -1) == "*" then + ext = string.sub(ext, 1, -2) + clean_line = string.sub(pipelessLine, 1, -2) + end + + return { + dir = curdir, + icon_col = to, + name_col = to, + lsp_col = to + #clean_line, + node = clean_line, + extension = ext, + type = M.TYPE_FILE, + } +end + +---@param line string +---@return Word|nil +M.get_node = function(line) + if string.find(line, '^"') then + return nil + end + + if line == "" then + return nil + end + + local curdir = vim.b.netrw_curdir + local liststyle = vim.b.netrw_liststyle + + if liststyle == 0 then + return parse_liststyle_0(line, curdir) + elseif liststyle == 1 then + return parse_liststyle_1(line, curdir) + elseif liststyle == 3 then + return parse_liststyle_3(line) + end + + return nil +end + +return M diff --git a/lua/netrw/init.lua b/lua/netrw/init.lua deleted file mode 100644 index 379d5a1..0000000 --- a/lua/netrw/init.lua +++ /dev/null @@ -1,149 +0,0 @@ -local M = {} -local parse = require("netrw.parse") - -local icon_provider = nil - -local function get_icon_provider(prefer) - local providers = {} - - local has_devicons, devicons = pcall(require, "nvim-web-devicons") - if has_devicons then - providers.devicons = devicons - end - - local has_miniicons, miniicons = pcall(require, "mini.icons") - if has_miniicons then - providers.miniicons = miniicons - end - - if prefer and providers[prefer] then - return { type = prefer, provider = providers[prefer] } - end - - if providers.miniicons then - return { type = "miniicons", provider = providers.miniicons } - elseif providers.devicons then - return { type = "devicons", provider = providers.devicons } - end - - return nil -end - -local function get_file_icon(node) - if not icon_provider then - return nil, nil - end - - if icon_provider.type == "devicons" then - local icon, hl = icon_provider.provider.get_icon(node.node, nil, { strict = true, default = false }) - return icon, hl - elseif icon_provider.type == "miniicons" then - local icon, hl = icon_provider.provider.get("file", node.node) - return icon, hl - end - - return nil, nil -end - -local function get_lsp_diagnostics(bufnr, node) - local diagnostics = {} - local filepath = node.dir .. "/" .. node.node - - local diag = vim.diagnostic.get(nil, { path = filepath }) - - for _, d in ipairs(diag) do - if d.severity == vim.diagnostic.severity.ERROR and M.options.lsp.error then - table.insert(diagnostics, { icon = "E", hl = "DiagnosticError" }) - elseif d.severity == vim.diagnostic.severity.WARN and M.options.lsp.warn then - table.insert(diagnostics, { icon = "W", hl = "DiagnosticWarn" }) - elseif d.severity == vim.diagnostic.severity.INFO and M.options.lsp.info then - table.insert(diagnostics, { icon = "I", hl = "DiagnosticInfo" }) - elseif d.severity == vim.diagnostic.severity.HINT and M.options.lsp.hint then - table.insert(diagnostics, { icon = "H", hl = "DiagnosticHint" }) - end - end - - return diagnostics -end - -local function draw(bufnr) - local namespace = vim.api.nvim_create_namespace("netrw") - vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - - for i, line in ipairs(lines) do - local node = parse.get_node(line) - if node then - -- Draw file/dir icon on the left - if (node.type == parse.TYPE_FILE and M.options.file) or - (node.type == parse.TYPE_DIR and M.options.dir) then - local icon, hl = get_file_icon(node) - if icon then - local opts = { - id = i * 2, -- Unique ID for icon - virt_text = hl and { { icon .. " ", hl } } or { { icon .. " " } }, - virt_text_pos = "inline", - } - vim.api.nvim_buf_set_extmark(bufnr, namespace, i - 1, node.icon_col, opts) - end - end - - -- Draw LSP diagnostics on the right - if node.type == parse.TYPE_FILE then - local diagnostics = get_lsp_diagnostics(bufnr, node) - if #diagnostics > 0 then - local virt_text = {} - for _, diag in ipairs(diagnostics) do - table.insert(virt_text, { " " .. diag.icon, diag.hl }) - end - - local opts = { - id = i * 2 + 1, -- Unique ID for LSP - virt_text = virt_text, - virt_text_pos = "inline", - } - vim.api.nvim_buf_set_extmark(bufnr, namespace, i - 1, node.lsp_col, opts) - end - end - end - end -end - ---- @class Config -M.options = {} - ---- @class Config -local default = { - prefer = nil, -- "miniicons" or "devicons" - file = true, - dir = true, - lsp = { - info = false, - hint = false, - warn = true, - error = true, - } -} - ----@param options Config|nil -function M.setup(options) - M.options = vim.tbl_deep_extend("force", {}, default, options or {}) - - if M.options.file or M.options.dir then - icon_provider = get_icon_provider(M.options.prefer) - end - - vim.api.nvim_create_autocmd({ "BufEnter", "DiagnosticChanged" }, { - pattern = "*", - group = vim.api.nvim_create_augroup("netrw_icons", { clear = true }), - callback = function(args) - if vim.bo[args.buf].filetype ~= "netrw" then - return - end - draw(args.buf) - end - }) -end - -return M diff --git a/lua/netrw/parse.lua b/lua/netrw/parse.lua deleted file mode 100644 index c069dee..0000000 --- a/lua/netrw/parse.lua +++ /dev/null @@ -1,185 +0,0 @@ -local M = {} - -M.TYPE_DIR = 0 -M.TYPE_FILE = 1 -M.TYPE_SYMLINK = 2 - ----@alias Word {dir:string, node:string, link:string|nil, extension:string|nil, type:number, icon_col:number, name_col:number, lsp_col:number} - ----@param line string ----@param curdir string ----@return Word|nil -local parse_liststyle_0 = function(line, curdir) - local _, _, node, link = string.find(line, "^(.+)@\t%s*%-%->%s*(.+)") - if node then - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #node, - node = node, - extension = vim.fn.fnamemodify(node, ":e"), - link = link, - type = M.TYPE_SYMLINK, - } - end - - local _, _, dir = string.find(line, "^(.*)/") - if dir then - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #dir + 1, -- +1 for the "/" - node = dir, - type = M.TYPE_DIR, - } - end - - local ext = vim.fn.fnamemodify(line, ":e") - local clean_line = line - if string.sub(ext, -1) == "*" then - ext = string.sub(ext, 1, -2) - clean_line = string.sub(line, 1, -2) - end - - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #clean_line, - node = clean_line, - extension = ext, - type = M.TYPE_FILE, - } -end - ----@param line string ----@param curdir string ----@return Word|nil -local parse_liststyle_1 = function(line, curdir) - local _, _, node, link = string.find(line, "^(.+)@%s+") - if node then - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #node, - node = node, - extension = vim.fn.fnamemodify(node, ":e"), - link = link, - type = M.TYPE_SYMLINK, - } - end - - local _, _, dir = string.find(line, "^(.*)/") - if dir then - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #dir + 1, - node = dir, - type = M.TYPE_DIR, - } - end - - local file = vim.fn.substitute(line, "^\\(\\%(\\S\\+ \\)*\\S\\+\\).\\{-}$", "\\1", "e") - local ext = vim.fn.fnamemodify(file, ":e") - if string.sub(ext, -1) == "*" then - ext = string.sub(ext, 1, -2) - file = string.sub(file, 1, -2) - end - - return { - dir = curdir, - icon_col = 0, - name_col = 0, - lsp_col = #file, - node = file, - extension = ext, - type = M.TYPE_FILE, - } -end - ----@param line string ----@return Word|nil -local parse_liststyle_3 = function(line) - local curdir = vim.b.netrw_curdir - local _, to = string.find(line, "^[|%s]*") - local pipelessLine = string.sub(line, to + 1, #line) - - if pipelessLine == "" then - return nil - end - - local _, _, node, link = string.find(pipelessLine, "^(.+)@\t%s*%-%->%s*(.+)") - if node then - return { - dir = curdir, - icon_col = to, - name_col = to, - lsp_col = to + #node, - node = node, - extension = vim.fn.fnamemodify(node, ":e"), - link = link, - type = M.TYPE_SYMLINK, - } - end - - local _, _, dir = string.find(pipelessLine, "^(.*)/") - if dir then - return { - dir = curdir, - icon_col = to, - name_col = to, - lsp_col = to + #dir + 1, - node = dir, - type = M.TYPE_DIR, - } - end - - local ext = vim.fn.fnamemodify(pipelessLine, ":e") - local clean_line = pipelessLine - if string.sub(ext, -1) == "*" then - ext = string.sub(ext, 1, -2) - clean_line = string.sub(pipelessLine, 1, -2) - end - - return { - dir = curdir, - icon_col = to, - name_col = to, - lsp_col = to + #clean_line, - node = clean_line, - extension = ext, - type = M.TYPE_FILE, - } -end - ----@param line string ----@return Word|nil -M.get_node = function(line) - if string.find(line, '^"') then - return nil - end - - if line == "" then - return nil - end - - local curdir = vim.b.netrw_curdir - local liststyle = vim.b.netrw_liststyle - - if liststyle == 0 then - return parse_liststyle_0(line, curdir) - elseif liststyle == 1 then - return parse_liststyle_1(line, curdir) - elseif liststyle == 3 then - return parse_liststyle_3(line) - end - - return nil -end - -return M -- cgit v1.3