initial commit
This commit is contained in:
commit
313c9508b4
|
@ -0,0 +1,62 @@
|
|||
## Lf.nvim
|
||||
|
||||
This is a neovim plugin for the [`lf`](https://github.com/gokcehan/lf) file manager.
|
||||
It is very similar to [`lf.vim`](https://github.com/ptzz/lf.vim), except for that this is written in Lua.
|
||||
|
||||
**NOTE**: This plugin uses [`toggleterm.nvim`](https://github.com/akinsho/toggleterm.nvim) and [`plenary.nvim`](https://github.com/nvim-lua/plenary.nvim)
|
||||
|
||||
### Setup/Configuration
|
||||
|
||||
```lua
|
||||
local M = {}
|
||||
|
||||
-- Defaults
|
||||
local lf = require("lf").setup({
|
||||
default_cmd = "lf", -- default `lf` command
|
||||
default_action = "edit", -- default action when `Lf` opens a file
|
||||
default_actions = { -- default action keybindings
|
||||
["<C-t>"] = "tabedit",
|
||||
["<C-x>"] = "split",
|
||||
["<C-v>"] = "vsplit",
|
||||
["<C-o>"] = "tab drop",
|
||||
},
|
||||
|
||||
winblend = 10, -- psuedotransparency level
|
||||
dir = "", -- directory where `lf` starts ('gwd' is git-working-directory)
|
||||
direction = "float", -- window type: float horizontal vertical
|
||||
border = "double", -- border kind: single double shadow curved
|
||||
height = 0.80, -- height of the *floating* window
|
||||
width = 0.85, -- width of the *floating* window
|
||||
})
|
||||
|
||||
function M.start_lf()
|
||||
lf:start()
|
||||
end
|
||||
|
||||
vim.api.nvim_set_keymap("n", "<mapping>", "<cmd>lua require('file').start_lf()", { noremap = true })
|
||||
-- or
|
||||
vim.api.nvim_set_keymap("n", "<mapping>", "<cmd>lua require('lf').setup():start()", { noremap = true })
|
||||
|
||||
return M
|
||||
```
|
||||
|
||||
There is a command that does basically the exact same thing `:Lf`. This command takes one optional argument,
|
||||
which is a directory for `lf` to start in.
|
||||
|
||||
### Default Actions
|
||||
The goal is to allow for these keybindings to be hijacked by `Lf` and make them execute the command
|
||||
as soon as the keybinding is pressed; however, I am unsure of a way to do this at the moment. If `lf` had a more
|
||||
programmable API that was similar to `ranger`'s, then something like [`rnvimr`](https://github.com/kevinhwang91/rnvimr)
|
||||
would be possible, which allows this.
|
||||
|
||||
For the moment, these bindings are hijacked on the startup of `Lf`, and when they are pressed, a notification is sent
|
||||
that your default action has changed. When you go to open a file as you normally would, this command is ran instead
|
||||
of your `default_action`.
|
||||
|
||||
### Replacing Netrw
|
||||
The only configurable environment variable is `g:lf_replace_netrw`, which can be set to `1` to replace `netrw`
|
||||
|
||||
### TODO
|
||||
- `:LfToggle` command
|
||||
- Find a way for `lf` to hijack keybindings
|
||||
- Allow keybindings to cycle through various sizes of the terminal (similar to `rnvimr`)
|
|
@ -0,0 +1,240 @@
|
|||
local M = {}
|
||||
|
||||
---@diagnostic disable: redefined-local
|
||||
|
||||
local utils = require("lf.utils")
|
||||
local notify = utils.notify
|
||||
|
||||
local res, terminal = pcall(require, "toggleterm")
|
||||
if not res then
|
||||
notify("toggleterm.nvim must be installed to use this program", "error")
|
||||
return
|
||||
end
|
||||
|
||||
local res, Path = pcall(require, "plenary.path")
|
||||
if not res then
|
||||
notify("plenary must be installed to use this program", "error")
|
||||
return
|
||||
end
|
||||
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
local uv = vim.loop
|
||||
local g = vim.g
|
||||
local map = utils.map
|
||||
|
||||
---Error for this program
|
||||
ERROR = nil
|
||||
---Global running status
|
||||
---I'm unsure of a way to keep an `Lf` variable constant through more than 1 `setup` calls
|
||||
g.__lf_running = false
|
||||
|
||||
local Config = require("lf.config")
|
||||
|
||||
--- @class Terminal
|
||||
local Terminal = require("toggleterm.terminal").Terminal
|
||||
|
||||
--- @class Lf
|
||||
--- @field cmd string
|
||||
--- @field direction string the layout style for the terminal
|
||||
--- @field id number
|
||||
--- @field window number
|
||||
--- @field job_id number
|
||||
--- @field highlights table<string, table<string, string>>
|
||||
local Lf = {}
|
||||
|
||||
local function setup_term()
|
||||
terminal.setup(
|
||||
{
|
||||
size = function(term)
|
||||
if term.direction == "horizontal" then
|
||||
return vim.o.lines * 0.4
|
||||
elseif term.direction == "vertical" then
|
||||
return vim.o.columns * 0.5
|
||||
end
|
||||
end,
|
||||
hide_numbers = true,
|
||||
shade_filetypes = {},
|
||||
shade_terminals = true,
|
||||
shading_factor = "1",
|
||||
start_in_insert = true,
|
||||
insert_mappings = true,
|
||||
persist_size = true,
|
||||
-- open_mapping = [[<c-\>]],
|
||||
}
|
||||
)
|
||||
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
|
||||
---
|
||||
---@param config 'table'
|
||||
---@return Lf
|
||||
function Lf:new(config)
|
||||
local cfg = Config:set(config):get()
|
||||
self.__index = self
|
||||
|
||||
self.cfg = cfg
|
||||
self.cwd = uv.cwd()
|
||||
|
||||
setup_term()
|
||||
self.term = Terminal:new(
|
||||
{
|
||||
cmd = cfg.default_cmd,
|
||||
dir = cfg.dir,
|
||||
direction = cfg.direction,
|
||||
winblend = cfg.winblend,
|
||||
close_on_exit = true,
|
||||
|
||||
float_opts = {
|
||||
border = cfg.border,
|
||||
width = math.floor(vim.o.columns * cfg.width),
|
||||
height = math.floor(vim.o.lines * cfg.height),
|
||||
winblend = cfg.winblend,
|
||||
highlights = { border = "Normal", background = "Normal" },
|
||||
},
|
||||
|
||||
-- on_open = cfg.on_open,
|
||||
-- on_close = nil,
|
||||
}
|
||||
)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---Start the underlying terminal
|
||||
---@param path string path where lf starts (reads from config if none, else CWD)
|
||||
function Lf:start(path)
|
||||
self:__open_in(path or self.cfg.dir)
|
||||
if ERROR ~= nil then
|
||||
notify(ERROR, "error")
|
||||
return
|
||||
end
|
||||
self:__wrapper()
|
||||
|
||||
self.term.on_open = function(term)
|
||||
self:__on_open(term)
|
||||
end
|
||||
|
||||
self.term.on_exit = function(term, _, _, _)
|
||||
self:__callback(term)
|
||||
end
|
||||
|
||||
-- NOTE: Maybe pcall here?
|
||||
self.term:toggle()
|
||||
g.__lf_running = true
|
||||
end
|
||||
|
||||
function Lf:toggle(path)
|
||||
print(g.__lf_running)
|
||||
if g.__lf_running then
|
||||
self.term:close()
|
||||
g.__lf_running = false
|
||||
else
|
||||
self:start(path)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---Set the directory for `Lf` to open in
|
||||
---
|
||||
---@param path string
|
||||
---@return Lf
|
||||
function Lf:__open_in(path)
|
||||
path = Path:new(
|
||||
(function(dir)
|
||||
if dir == "gwd" then
|
||||
dir = require("lf.utils").git_dir()
|
||||
end
|
||||
|
||||
if dir then
|
||||
return fn.expand(dir)
|
||||
else
|
||||
return self.cwd
|
||||
end
|
||||
end)(path)
|
||||
)
|
||||
|
||||
if not path:exists() then
|
||||
ERROR = ("directory doesn't exist: %s"):format(path)
|
||||
return
|
||||
end
|
||||
|
||||
-- Should be fine, but just checking
|
||||
if not path:is_dir() then
|
||||
path = path:parent()
|
||||
end
|
||||
|
||||
self.term.dir = path:absolute()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@private
|
||||
---Wrap the default command value to write the selected files to a temporary file
|
||||
---
|
||||
---@return Lf
|
||||
function Lf:__wrapper()
|
||||
self.lf_tmp = os.tmpname()
|
||||
self.lastdir_tmp = os.tmpname()
|
||||
|
||||
self.term.cmd = ([[%s -last-dir-path='%s' -selection-path='%s' %s]]):format(
|
||||
self.term.cmd, self.lastdir_tmp, self.lf_tmp, self.term.dir
|
||||
)
|
||||
return self
|
||||
end
|
||||
|
||||
-- TODO: Figure out a way to open the file with these commands
|
||||
---On open closure to run in the `Terminal`
|
||||
---@param term Terminal
|
||||
function Lf:__on_open(term)
|
||||
for key, mapping in pairs(self.cfg.default_actions) do
|
||||
map(
|
||||
"t", key, function()
|
||||
self.cfg.default_action = mapping
|
||||
notify(("Default action changed: %s"):format(mapping))
|
||||
end, { noremap = true, buffer = term.bufnr }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---A callback for the `Terminal`
|
||||
---
|
||||
---@param term Terminal
|
||||
function Lf:__callback(term)
|
||||
if (self.cfg.default_action == "cd" or self.cfg.default_action == "lcd") and
|
||||
uv.fs_stat(self.lastdir_tmp) then
|
||||
local f = io.open(self.lastdir_tmp)
|
||||
local last_dir = f:read()
|
||||
f:close()
|
||||
|
||||
if last_dir ~= uv.cwd() then
|
||||
api.nvim_exec(("%s %s"):format(self.cfg.default_action, last_dir), true)
|
||||
return
|
||||
end
|
||||
elseif uv.fs_stat(self.lf_tmp) then
|
||||
local contents = {}
|
||||
|
||||
for line in io.lines(self.lf_tmp) do
|
||||
contents[#contents + 1] = line
|
||||
end
|
||||
|
||||
if not vim.tbl_isempty(contents) then
|
||||
term:close()
|
||||
|
||||
for _, fname in pairs(contents) do
|
||||
api.nvim_exec(
|
||||
("%s %s"):format(
|
||||
self.cfg.default_action, Path:new(fname):absolute()
|
||||
), true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.Lf = Lf
|
||||
|
||||
return M
|
|
@ -0,0 +1,53 @@
|
|||
--- @class Config
|
||||
--- @field default_cmd string default `lf` command
|
||||
--- @field default_action string default action when `Lf` opens a file
|
||||
--- @field default_actions table default action keybindings
|
||||
--- @field winblend number psuedotransparency level
|
||||
--- @field dir string directory where `lf` starts ('gwd' is git-working-directory)
|
||||
--- @field direction string window type: float horizontal vertical
|
||||
--- @field border string border kind: single double shadow curved
|
||||
--- @field height number height of the *floating* window
|
||||
--- @field width number width of the *floating* window
|
||||
local Config = {
|
||||
default_cmd = "lf",
|
||||
default_action = "edit",
|
||||
default_actions = {
|
||||
["<C-t>"] = "tabedit",
|
||||
["<C-x>"] = "split",
|
||||
["<C-v>"] = "vsplit",
|
||||
["<C-o>"] = "tab drop",
|
||||
},
|
||||
|
||||
winblend = 10,
|
||||
dir = "",
|
||||
direction = "float",
|
||||
border = "double",
|
||||
height = 0.80,
|
||||
width = 0.85,
|
||||
}
|
||||
|
||||
function Config:set(cfg)
|
||||
if cfg and type(cfg) ~= "table" then
|
||||
self = vim.tbl_deep_extend("force", self, cfg or {})
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Config:get(key)
|
||||
if key then
|
||||
return self[key]
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return setmetatable(
|
||||
Config, {
|
||||
__index = function(this, k)
|
||||
return this[k]
|
||||
end,
|
||||
__newindex = function(this, k, v)
|
||||
this[k] = v
|
||||
end,
|
||||
}
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
-- TODO: Cleanup set/new/start functions
|
||||
|
||||
return {
|
||||
setup = function(config)
|
||||
return require("lf.action").Lf:new(config)
|
||||
end,
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
local M = {}
|
||||
|
||||
-- This was taken from toggleterm.nvim
|
||||
|
||||
local fn = vim.fn
|
||||
local api = vim.api
|
||||
local fmt = string.format
|
||||
local levels = vim.log.levels
|
||||
|
||||
---Echo a message with `nvim_echo`
|
||||
---@param msg string message
|
||||
---@param hl string highlight group
|
||||
M.echomsg = function(msg, hl)
|
||||
hl = hl or "Title"
|
||||
api.nvim_echo({ { msg, hl } }, true, {})
|
||||
end
|
||||
|
||||
---Display an info message on the CLI
|
||||
---@param msg string
|
||||
M.info = function(msg)
|
||||
M.echomsg(("[INFO]: %s"):format(msg), "Directory")
|
||||
-- M.echomsg(("[INFO]: %s"):format(msg), "Identifier")
|
||||
end
|
||||
|
||||
---Display a warning message on the CLI
|
||||
---@param msg string
|
||||
M.warn = function(msg)
|
||||
M.echomsg(("[WARN]: %s"):format(msg), "WarningMsg")
|
||||
end
|
||||
|
||||
---Display an error message on the CLI
|
||||
---@param msg string
|
||||
M.err = function(msg)
|
||||
M.echomsg(("[ERR]: %s"):format(msg), "ErrorMsg")
|
||||
end
|
||||
|
||||
---Display notification message
|
||||
---@param msg string
|
||||
---@param level 'error' | 'info' | 'warn'
|
||||
M.notify = function(msg, level)
|
||||
level = level and levels[level:upper()] or levels.INFO
|
||||
vim.notify(fmt("[lf]: %s", msg), level)
|
||||
end
|
||||
|
||||
---Helper function to derive the current git directory path
|
||||
---@return string|nil
|
||||
M.git_dir = function()
|
||||
local gitdir = fn.system(
|
||||
fmt(
|
||||
"git -C %s rev-parse --show-toplevel", fn.expand("%:p:h")
|
||||
)
|
||||
)
|
||||
|
||||
local isgitdir = fn.matchstr(gitdir, "^fatal:.*") == ""
|
||||
if not isgitdir then
|
||||
return
|
||||
end
|
||||
return vim.trim(gitdir)
|
||||
end
|
||||
|
||||
M.map = function(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
|
||||
|
||||
return M
|
|
@ -0,0 +1,16 @@
|
|||
command! -nargs=* -complete=file Lf lua require('lf').setup():start(<f-args>)
|
||||
|
||||
" TODO: Finish this command
|
||||
" command! -nargs=* -complete=file LfToggle lua require('lf').setup():toggle(<f-args>)
|
||||
|
||||
" TODO: Make sure that this works
|
||||
if exists('g:lf_replace_netrw') && g:lf_replace_netrw
|
||||
augroup ReplaceNetrwWithLf
|
||||
autocmd VimEnter * silent! autocmd! FileExplorer
|
||||
autocmd BufEnter * let s:buf_path = expand("%")
|
||||
\ | if isdirectory(s:buf_path)
|
||||
\ | bdelete!
|
||||
\ | call timer_start(100, {->v:lua.require'lf'.setup():start(s:buf_path)})
|
||||
\ | endif
|
||||
augroup END
|
||||
endif
|
Reference in New Issue