From 18c465b922a3b3762ffa3dd7ee015f39dbba107d Mon Sep 17 00:00:00 2001 From: Luca Bilke Date: Sun, 28 Jan 2024 23:11:42 +0100 Subject: [PATCH] fixes --- lua/.luarc.json | 4 - lua/lf.lua | 64 ++++++++++++- lua/lf/config.lua | 11 --- lua/lf/main.lua | 134 ++++++---------------------- lua/lf/meta.lua | 82 +++++++++++++++++ lua/lf/utils.lua | 222 ---------------------------------------------- 6 files changed, 172 insertions(+), 345 deletions(-) create mode 100644 lua/lf/meta.lua delete mode 100644 lua/lf/utils.lua diff --git a/lua/.luarc.json b/lua/.luarc.json index f91ebc1..c7da2db 100644 --- a/lua/.luarc.json +++ b/lua/.luarc.json @@ -1,9 +1,5 @@ { "diagnostics.globals": [ "vim" - ], - "diagnostics.disable": [ - "duplicate-doc-field", - "duplicate-set-field" ] } diff --git a/lua/lf.lua b/lua/lf.lua index d6395a8..6c58dd5 100644 --- a/lua/lf.lua +++ b/lua/lf.lua @@ -4,6 +4,22 @@ local Config = require("lf.config") local api = vim.api +---Make `Lf` become the file manager that opens whenever a directory buffer is loaded +---@param bufnr integer +---@return boolean +local function become_dir_fman(bufnr) + local bufname = api.nvim_buf_get_name(bufnr) + if bufname == "" then + return false + end + local stat = vim.loop.fs_stat(bufname) + if type(stat) ~= "table" or (type(stat) == "table" and stat.type ~= "directory") then + return false + end + + return true +end + ---Setup the Lf plugin ---@param cfg Lf.Config function M.setup(cfg) @@ -14,6 +30,52 @@ function M.setup(cfg) cfg = cfg or {} M.__conf = cfg Config.init() + api.nvim_create_user_command("Lf", function(tbl) + require("lf").start(tbl.args) + end, { nargs = "*", complete = "file" }) + + if Config.replace_netrw then + local group = api.nvim_create_augroup("Lf_ReplaceNetrw", { clear = true }) + + if vim.g.loaded_netrwPlugin ~= 1 then + api.nvim_create_autocmd("FileType", { + desc = "Display message about Lf not being default file manager", + group = group, + pattern = "netrw", + once = true, + callback = function() + api.nvim_err_writeln([[Lf cannot be the default file manager with netrw enabled.]] .. + [[Put `vim.g.loaded_netrwPlugin` in your configuration.]]) + end, + }) + end + + api.nvim_create_autocmd("VimEnter", { + desc = "Override the default file manager (i.e., netrw)", + group = group, + pattern = "*", + nested = true, + callback = function(a) + if vim.fn.exists("#FileExplorer") then + api.nvim_create_augroup("FileExplorer", { clear = true }) + end + end, + }) + + api.nvim_create_autocmd("BufEnter", { + desc = "After overriding default file manager, open Lf", + group = group, + pattern = "*", + once = true, + callback = function(a) + if become_dir_fman(a.buf) then + vim.defer_fn(function() + require("lf").start(a.file) + end, 1) + end + end, + }) + end end ---Start the file manager @@ -44,7 +106,7 @@ function M.start(path, cfg) return end - local opts = vim.tbl_deep_extend("keep", cfg or {}, Config.data) + cfg = vim.tbl_deep_extend("keep", cfg or {}, Config.data) Lf:new(cfg or M.__conf):start(path) end end diff --git a/lua/lf/config.lua b/lua/lf/config.lua index 7120d40..f78ce3c 100644 --- a/lua/lf/config.lua +++ b/lua/lf/config.lua @@ -14,7 +14,6 @@ local default = { default_cmd = "lf", default_action = "drop", dir = "gwd", - escape_quit = false, focus_on_open = true, count = nil, } @@ -26,7 +25,6 @@ local function validate(cfg) default_cmd = {cfg.default_cmd, "s", false}, default_action = {cfg.default_action, "s", false}, dir = {cfg.dir, "s", false}, - escape_quit = {cfg.escape_quit, "b", false}, focus_on_open = {cfg.focus_on_open, "b", false}, count = {cfg.count, "n", true}, }) @@ -76,12 +74,3 @@ return setmetatable(Config, { api.nvim_err_writeln(("do not set invalid config values: %s => %s"):format(key, val)) end, }) - ----@alias Lf.directory "'gwd'"|"''"|nil|string ----@class Lf.Config ----@field default_cmd? string Default `lf` command ----@field default_action? string Default action when `Lf` opens a file ----@field dir? Lf.directory Directory where `lf` starts ('gwd' is git-working-directory, ""/nil is CWD) ----@field escape_quit? boolean Whether escape should be mapped to quit ----@field focus_on_open? boolean Whether Lf should open focused on current file ----@field count? integer A number that triggers that specific terminal diff --git a/lua/lf/main.lua b/lua/lf/main.lua index a714d09..692bcbb 100644 --- a/lua/lf/main.lua +++ b/lua/lf/main.lua @@ -1,6 +1,3 @@ -local utils = require("lf.utils") -local fs = utils.fs - local cmd = vim.cmd local api = vim.api local fn = vim.fn @@ -14,105 +11,12 @@ if not ok then end local Config = require("lf.config") --- local promise = require("promise") --- local async = require("async") ----@alias Mode "n" | "i" | "?" - ---- @class TerminalState ---- @field mode Mode - ---- @class TermCreateArgs ---- @field newline_chr? string user specified newline chararacter ---- @field cmd? string a custom command to run ---- @field direction? string the layout style for the terminal ---- @field id number? ---- @field highlights table>? ---- @field dir string? the directory for the terminal ---- @field count number? the count that triggers that specific terminal ---- @field display_name string? ---- @field hidden boolean? whether or not to include this terminal in the terminals list ---- @field close_on_exit boolean? whether or not to close the terminal window when the process exits ---- @field auto_scroll boolean? whether or not to scroll down on terminal output ---- @field float_opts table? ---- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)? ---- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)? ---- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)? ---- @field on_create fun(term:Terminal)? ---- @field on_open fun(term:Terminal)? ---- @field on_close fun(term:Terminal)? - ---- @class Terminal ---- @field newline_chr string ---- @field cmd string ---- @field direction string the layout style for the terminal ---- @field id number ---- @field bufnr number ---- @field window number ---- @field job_id number ---- @field highlights table> ---- @field dir string the directory for the terminal ---- @field name string the name of the terminal ---- @field count number the count that triggers that specific terminal ---- @field hidden boolean whether or not to include this terminal in the terminals list ---- @field close_on_exit boolean? whether or not to close the terminal window when the process exits ---- @field auto_scroll boolean? whether or not to scroll down on terminal output ---- @field float_opts table? ---- @field display_name string? ---- @field env table environmental variables passed to jobstart() ---- @field clear_env boolean use clean job environment, passed to jobstart() ---- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)? ---- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)? ---- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)? ---- @field on_create fun(term:Terminal)? ---- @field on_open fun(term:Terminal)? ---- @field on_close fun(term:Terminal)? ---- @field _display_name fun(term: Terminal): string ---- @field __state TerminalState ---- @field new fun(self, term: TermCreateArgs?): Terminal ---- @field close fun(self) ---- @field open fun(self, size: number?, direction: string?) local Terminal = require("toggleterm.terminal").Terminal ----@class Lf ----@field cfg Lf.Config Configuration options ----@field term Terminal toggleterm terminal ----@field view_idx number Current index of configuration `views` ----@field winid? number `Terminal` window id ----@field curfile? string File path of the currently opened file ----@field tmp_id? string Path to a file containing `lf`'s id ----@field tmp_sel string Path to a file containing `lf`'s selection(s) ----@field tmp_lastdir string Path to a file containing the last directory `lf` was in ----@field id number? Current `lf` session id ----@field bufnr number The open file's buffer number ----@field arglist string[] The argument list to neovim ----@field action string The current action to open the file ----@field signcolumn string The signcolumn set by the user before the terminal buffer overrides it local Lf = {} Lf.__index = Lf ----@private ----Setup `toggleterm`'s `Terminal` -local function setup_term() - terminal.setup({ - size = function(term) - if term.direction == "horizontal" then - return o.lines * 0.4 - elseif term.direction == "vertical" then - return o.columns * 0.5 - end - end, - hide_numbers = true, - shade_filetypes = {}, - shade_terminals = false, - start_in_insert = true, - insert_mappings = false, - terminal_mappings = true, - persist_mode = false, - persist_size = false, - }) -end - ---Setup a new instance of `Lf` ---Configuration has not been fully parsed by the end of this function ---A `Terminal` becomes attached and is able to be toggled @@ -128,10 +32,11 @@ function Lf:new(config) self:__set_argv() self.bufnr = 0 self.view_idx = 1 + self.action = self.cfg.default_action -- Needs to be grabbed here before the terminal buffer is created self.signcolumn = o.signcolumn - setup_term() + terminal.setup() self:__create_term() return self @@ -144,8 +49,7 @@ function Lf:__create_term() cmd = self.cfg.default_cmd, dir = self.cfg.dir, close_on_exit = true, - hidden = false, - display_name = "Lf", + hidden = true, count = self.cfg.count, }) end @@ -176,7 +80,13 @@ end ---@return Lf? function Lf:__open_in(path) if path == "gwd" or path == "git_dir" then - path = utils.git_dir() + local gitdir = fn.system(("git -C %s rev-parse --show-toplevel"):format(fn.expand("%:p:h"))) + + if gitdir:match("^fatal:.*") then + api.nvim_out_write("Failed to get git directory") + return + end + path = vim.trim(gitdir) end if type(path) == "string" then @@ -187,14 +97,14 @@ function Lf:__open_in(path) local stat = uv.fs_stat(path) if not type(stat) == "table" then - local cwd = uv.cwd() + local cwd = uv.cwd() --[[@as string]] stat = uv.fs_stat(cwd) - path = cwd --[[@as string]] + path = cwd end -- Should be fine, but just checking if stat and not stat.type == "directory" then - path = fs.dirname(path) + path = fn.fnamemodify(path, ":h") end self.term.dir = path @@ -214,7 +124,7 @@ function Lf:__set_cmd_wrapper() local open_on = self.term.dir if self.cfg.focus_on_open - and fs.dirname(self.curfile) == self.term.dir + and fn.fnamemodify(self.curfile, ":h") == self.term.dir then open_on = self.curfile end @@ -261,6 +171,16 @@ function Lf:__on_open(term) -- end, 800) end +local function read_file(path) + -- tonumber(444, 8) == 292 + local fd = assert(uv.fs_open(fn.expand(path), "r", 292)) + local stat = assert(uv.fs_fstat(fd)) + local buffer = assert(uv.fs_read(fd, stat.size, 0)) + uv.fs_close(fd) + return buffer, stat +end + + ---@private ---A callback for the `Terminal` --- @@ -270,7 +190,7 @@ function Lf:__callback(term) (self.action == "cd" or self.action == "lcd") and uv.fs_stat(self.tmp_lastdir) then - local last_dir = utils.read_file(self.tmp_lastdir) + local last_dir = read_file(self.tmp_lastdir) if type(last_dir) == "string" then cmd(("%s %s"):format(self.action, fn.fnameescape(last_dir))) return @@ -284,8 +204,8 @@ function Lf:__callback(term) cmd(("%s %s"):format(self.action, fesc)) local args = table.concat(self.arglist, " ") if string.len(args) > 0 then - cmd.argadd(args) - cmd.argdedupe() + cmd.argadd(args) + cmd.argdedupe() end self:__set_argv() end diff --git a/lua/lf/meta.lua b/lua/lf/meta.lua new file mode 100644 index 0000000..3835837 --- /dev/null +++ b/lua/lf/meta.lua @@ -0,0 +1,82 @@ +---@meta + +---@alias Mode "n" | "i" | "?" + +--- @class TerminalState +--- @field mode Mode + +--- @class TermCreateArgs +--- @field newline_chr? string user specified newline chararacter +--- @field cmd? string a custom command to run +--- @field direction? string the layout style for the terminal +--- @field id number? +--- @field highlights table>? +--- @field dir string? the directory for the terminal +--- @field count number? the count that triggers that specific terminal +--- @field display_name string? +--- @field hidden boolean? whether or not to include this terminal in the terminals list +--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits +--- @field auto_scroll boolean? whether or not to scroll down on terminal output +--- @field float_opts table? +--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)? +--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)? +--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)? +--- @field on_create fun(term:Terminal)? +--- @field on_open fun(term:Terminal)? +--- @field on_close fun(term:Terminal)? + +--- @class Terminal +--- @field newline_chr string +--- @field cmd string +--- @field direction string the layout style for the terminal +--- @field id number +--- @field bufnr number +--- @field window number +--- @field job_id number +--- @field highlights table> +--- @field dir string the directory for the terminal +--- @field name string the name of the terminal +--- @field count number the count that triggers that specific terminal +--- @field hidden boolean whether or not to include this terminal in the terminals list +--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits +--- @field auto_scroll boolean? whether or not to scroll down on terminal output +--- @field float_opts table? +--- @field display_name string? +--- @field env table environmental variables passed to jobstart() +--- @field clear_env boolean use clean job environment, passed to jobstart() +--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)? +--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)? +--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)? +--- @field on_create fun(term:Terminal)? +--- @field on_open fun(term:Terminal)? +--- @field on_close fun(term:Terminal)? +--- @field _display_name fun(term: Terminal): string +--- @field __state TerminalState +--- @field new fun(self, term: TermCreateArgs?): Terminal +--- @field close fun(self) +--- @field open fun(self, size: number?, direction: string?) + +---@alias Lf.directory "'gwd'"|"''"|nil|string + +---@class Lf.Config +---@field default_cmd? string Default `lf` command +---@field default_action? string Default action when `Lf` opens a file +---@field dir? Lf.directory Directory where `lf` starts ('gwd' is git-working-directory, ""/nil is CWD) +---@field focus_on_open? boolean Whether Lf should open focused on current file +---@field count? integer A number that triggers that specific terminal + +---@class Lf +---@field cfg Lf.Config Configuration options +---@field term Terminal toggleterm terminal +---@field view_idx number Current index of configuration `views` +---@field winid? number `Terminal` window id +---@field curfile? string File path of the currently opened file +---@field tmp_id? string Path to a file containing `lf`'s id +---@field tmp_sel string Path to a file containing `lf`'s selection(s) +---@field tmp_lastdir string Path to a file containing the last directory `lf` was in +---@field id number? Current `lf` session id +---@field bufnr number The open file's buffer number +---@field arglist string[] The argument list to neovim +---@field action string The current action to open the file +---@field signcolumn string The signcolumn set by the user before the terminal buffer overrides it +---@field start fun(self, path: string?) diff --git a/lua/lf/utils.lua b/lua/lf/utils.lua deleted file mode 100644 index 3bd3a87..0000000 --- a/lua/lf/utils.lua +++ /dev/null @@ -1,222 +0,0 @@ -local M = {} --- -local uv = vim.loop -local fn = vim.fn --- local api = vim.api --- local levels = vim.log.levels --- local o = vim.o --- --- ---Echo a message with `nvim_echo` --- ---@param msg string message --- ---@param hl string highlight group --- function M.echomsg(msg, hl) --- hl = hl or "Title" --- api.nvim_echo({{msg, hl}}, true, {}) --- end --- --- ---Display notification message --- ---@param msg string --- ---@param level number --- ---@param opts table? --- function M.notify(msg, level, opts) --- opts = vim.tbl_extend("force", opts or {}, {title = "lf.nvim"}) --- vim.notify(msg, level, opts) --- end --- --- ---INFO message --- ---@param msg string --- ---@param print? boolean --- ---@param opts table? --- function M.info(msg, print, opts) --- if print then --- M.echomsg(("[INFO]: %s"):format(msg), "Directory") --- else --- M.notify(msg, levels.INFO, opts) --- end --- end --- --- ---WARN message --- ---@param msg string --- ---@param print? boolean --- ---@param opts table? --- function M.warn(msg, print, opts) --- if print then --- M.echomsg(("[WARN]: %s"):format(msg), "WarningMsg") --- else --- M.notify(msg, levels.WARN, opts) --- end --- end --- --- ---ERROR message --- ---@param msg string --- ---@param print? boolean --- ---@param opts table? --- function M.err(msg, print, opts) --- if print then --- M.echomsg(("[ERROR]: %s"):format(msg), "ErrorMsg") --- else --- M.notify(msg, levels.ERROR, opts) --- end --- end --- ----@param path string ----@diagnostic disable-next-line: undefined-doc-name ----@return uv_fs_t|string ----@diagnostic disable-next-line: undefined-doc-name ----@return uv.aliases.fs_stat_table? -function M.read_file(path) - -- tonumber(444, 8) == 292 - local fd = assert(uv.fs_open(fn.expand(path), "r", 292)) - local stat = assert(uv.fs_fstat(fd)) - local buffer = assert(uv.fs_read(fd, stat.size, 0)) - uv.fs_close(fd) - return buffer, stat -end --- --- ---Create a neovim keybinding --- ---@param mode string vim mode in a single letter --- ---@param lhs string keys that are bound --- ---@param rhs string|function string or lua function that is mapped to the keys --- ---@param opts table? options set for the mapping --- function M.map(mode, lhs, rhs, opts) --- opts = opts or {} --- opts.noremap = opts.noremap == nil and true or opts.noremap --- vim.keymap.set(mode, lhs, rhs, opts) --- end --- --- ---Get Neovim window height --- ---@return number --- function M.height() --- return o.lines - o.cmdheight --- end --- --- ---Get neovim window width (minus signcolumn) --- ---@param bufnr number: Buffer number from the file that Lf is opened from --- ---@param signcolumn string: Signcolumn option set by the user, not the terminal buffer --- ---@return number --- function M.width(bufnr, signcolumn) --- if signcolumn:match("no") then --- return o.columns --- end --- --- -- This is a rough estimate of the signcolumn --- local width = #tostring(api.nvim_buf_line_count(bufnr)) --- local col = vim.split(signcolumn, ":") --- if #col == 2 then --- width = width + tonumber(col[2]) --- end --- --- return o.columns - width --- end --- --- ---Get the table that is passed to `api.nvim_win_set_config` --- ---@param opts table --- ---@param bufnr number Buffer number from the file that Lf is opened from --- ---@param signcolumn string Signcolumn option set by the user, not the terminal buffer --- ---@return table --- function M.get_view(opts, bufnr, signcolumn) --- opts = opts or {} --- --- local width = opts.width --- if width > o.columns then --- M.err(("width (%d) cannot be greater than columns (%d)"):format(width, o.columns)) --- width = nil --- end --- width = width or M.width(bufnr, signcolumn) --- if width < 0 then --- if width > -1 then --- width = fn.float2nr(o.columns + fn.round(width * o.columns)) --- else --- width = o.columns + width --- end --- else --- if width < 1 then --- width = fn.float2nr(fn.round(width * o.columns)) --- end --- end --- --- local height = opts.height --- if height > o.lines then --- M.err(("height (%d) cannot be greater than lines (%d)"):format(height, o.lines)) --- height = nil --- end --- height = height or M.height() --- if height < 0 then --- if height > -1 then --- height = fn.float2nr(o.lines + fn.round(height * o.lines)) --- else --- height = o.lines + height --- end --- else --- if height < 1 then --- height = fn.float2nr(fn.round(height * o.lines)) --- end --- end --- --- local col = opts.col --- and fn.float2nr(fn.round(opts.col * o.columns)) --- or math.ceil(o.columns - width) * 0.5 - 1 --- local row = opts.row --- and fn.float2nr(fn.round(opts.row * o.lines)) --- or math.ceil(o.lines - height) * 0.5 - 1 --- --- return { --- col = col, -- fractional allowed --- row = row, -- fractional allowed --- width = width, -- minimum of 1 --- height = height, -- minimum of 1 --- relative = "editor", --- style = "minimal", --- } --- end --- --- ---@return string|nil --- function M.git_dir() --- ---@diagnostic disable-next-line: missing-parameter --- local gitdir = fn.system(("git -C %s rev-parse --show-toplevel"):format(fn.expand("%:p:h"))) --- --- if gitdir:match("^fatal:.*") then --- M.info("Failed to get git directory") --- return --- end --- return vim.trim(gitdir) --- end --- --- ---Set the tmux statusline when opening/closing `Lf` --- ---@param disable boolean: whether the statusline is being enabled or disabled --- function M.tmux(disable) --- if not vim.env.TMUX then --- return --- end --- if disable then --- fn.system([[tmux set status off]]) --- fn.system([[tmux list-panes -F '\#F' | grep -q Z || tmux resize-pane -Z]]) --- else --- fn.system([[tmux set status on]]) --- fn.system([[tmux list-panes -F '\#F' | grep -q Z && tmux resize-pane -Z]]) --- end --- end --- --- -- ╭──────────────────────────────────────────────────────────╮ --- -- │ vim.fs replacement │ --- -- ╰──────────────────────────────────────────────────────────╯ --- --- M.fs = {} --- --- ---Return basename of the given file entry --- --- --- ---@param file string: File or directory --- ---@return string: Basename of `file` --- function M.fs.basename(file) --- return fn.fnamemodify(file, ":t") --- end - ----Return parent directory of the given file entry ---- ----@param file string: File or directory ----@return string: Parent directory of file -function M.fs.dirname(file) - return fn.fnamemodify(file, ":h") -end - -return M