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