363 lines
9.9 KiB
Lua
363 lines
9.9 KiB
Lua
-- Copyright 2019 Yazdani Kiyan under MIT License
|
|
local lib = require "nvim-tree.lib"
|
|
local notify = require "nvim-tree.notify"
|
|
local utils = require "nvim-tree.utils"
|
|
local view = require "nvim-tree.view"
|
|
|
|
local M = {}
|
|
|
|
local function get_user_input_char()
|
|
local c = vim.fn.getchar()
|
|
while type(c) ~= "number" do
|
|
c = vim.fn.getchar()
|
|
end
|
|
return vim.fn.nr2char(c)
|
|
end
|
|
|
|
---Get all windows in the current tabpage that aren't NvimTree.
|
|
---@return table with valid win_ids
|
|
local function usable_win_ids()
|
|
local tabpage = vim.api.nvim_get_current_tabpage()
|
|
local win_ids = vim.api.nvim_tabpage_list_wins(tabpage)
|
|
local tree_winid = view.get_winnr(tabpage)
|
|
|
|
return vim.tbl_filter(function(id)
|
|
local bufid = vim.api.nvim_win_get_buf(id)
|
|
for option, v in pairs(M.window_picker.exclude) do
|
|
local ok, option_value = pcall(vim.api.nvim_buf_get_option, bufid, option)
|
|
if ok and vim.tbl_contains(v, option_value) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local win_config = vim.api.nvim_win_get_config(id)
|
|
return id ~= tree_winid and win_config.focusable and not win_config.external
|
|
end, win_ids)
|
|
end
|
|
|
|
---Find the first window in the tab that is not NvimTree.
|
|
---@return integer -1 if none available
|
|
local function first_win_id()
|
|
local selectable = usable_win_ids()
|
|
if #selectable > 0 then
|
|
return selectable[1]
|
|
else
|
|
return -1
|
|
end
|
|
end
|
|
|
|
---Get user to pick a window in the tab that is not NvimTree.
|
|
---@return integer|nil -- If a valid window was picked, return its id. If an
|
|
--- invalid window was picked / user canceled, return nil. If there are
|
|
--- no selectable windows, return -1.
|
|
local function pick_win_id()
|
|
local selectable = usable_win_ids()
|
|
|
|
-- If there are no selectable windows: return. If there's only 1, return it without picking.
|
|
if #selectable == 0 then
|
|
return -1
|
|
end
|
|
if #selectable == 1 then
|
|
return selectable[1]
|
|
end
|
|
|
|
if #M.window_picker.chars < #selectable then
|
|
notify.error(string.format("More windows (%d) than actions.open_file.window_picker.chars (%d).", #selectable, #M.window_picker.chars))
|
|
return nil
|
|
end
|
|
|
|
local i = 1
|
|
local win_opts = {}
|
|
local win_map = {}
|
|
local laststatus = vim.o.laststatus
|
|
vim.o.laststatus = 2
|
|
|
|
local tabpage = vim.api.nvim_get_current_tabpage()
|
|
local win_ids = vim.api.nvim_tabpage_list_wins(tabpage)
|
|
|
|
local not_selectable = vim.tbl_filter(function(id)
|
|
return not vim.tbl_contains(selectable, id)
|
|
end, win_ids)
|
|
|
|
if laststatus == 3 then
|
|
for _, win_id in ipairs(not_selectable) do
|
|
local ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline")
|
|
local ok_hl, winhl = pcall(vim.api.nvim_win_get_option, win_id, "winhl")
|
|
|
|
win_opts[win_id] = {
|
|
statusline = ok_status and statusline or "",
|
|
winhl = ok_hl and winhl or "",
|
|
}
|
|
|
|
-- Clear statusline for windows not selectable
|
|
vim.api.nvim_win_set_option(win_id, "statusline", " ")
|
|
end
|
|
end
|
|
|
|
-- Setup UI
|
|
for _, id in ipairs(selectable) do
|
|
local char = M.window_picker.chars:sub(i, i)
|
|
local ok_status, statusline = pcall(vim.api.nvim_win_get_option, id, "statusline")
|
|
local ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl")
|
|
|
|
win_opts[id] = {
|
|
statusline = ok_status and statusline or "",
|
|
winhl = ok_hl and winhl or "",
|
|
}
|
|
win_map[char] = id
|
|
|
|
vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=")
|
|
vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker")
|
|
|
|
i = i + 1
|
|
if i > #M.window_picker.chars then
|
|
break
|
|
end
|
|
end
|
|
|
|
vim.cmd "redraw"
|
|
if vim.opt.cmdheight._value ~= 0 then
|
|
print "Pick window: "
|
|
end
|
|
local _, resp = pcall(get_user_input_char)
|
|
resp = (resp or ""):upper()
|
|
utils.clear_prompt()
|
|
|
|
-- Restore window options
|
|
for _, id in ipairs(selectable) do
|
|
for opt, value in pairs(win_opts[id]) do
|
|
vim.api.nvim_win_set_option(id, opt, value)
|
|
end
|
|
end
|
|
|
|
if laststatus == 3 then
|
|
for _, id in ipairs(not_selectable) do
|
|
for opt, value in pairs(win_opts[id]) do
|
|
vim.api.nvim_win_set_option(id, opt, value)
|
|
end
|
|
end
|
|
end
|
|
|
|
vim.o.laststatus = laststatus
|
|
|
|
if not vim.tbl_contains(vim.split(M.window_picker.chars, ""), resp) then
|
|
return
|
|
end
|
|
|
|
return win_map[resp]
|
|
end
|
|
|
|
local function open_file_in_tab(filename)
|
|
if M.quit_on_open then
|
|
view.close()
|
|
end
|
|
vim.cmd("tabe " .. vim.fn.fnameescape(filename))
|
|
end
|
|
|
|
local function drop(filename)
|
|
if M.quit_on_open then
|
|
view.close()
|
|
end
|
|
vim.cmd("drop " .. vim.fn.fnameescape(filename))
|
|
end
|
|
|
|
local function tab_drop(filename)
|
|
if M.quit_on_open then
|
|
view.close()
|
|
end
|
|
vim.cmd("tab :drop " .. vim.fn.fnameescape(filename))
|
|
end
|
|
|
|
local function on_preview(buf_loaded)
|
|
if not buf_loaded then
|
|
vim.bo.bufhidden = "delete"
|
|
|
|
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
|
group = vim.api.nvim_create_augroup("RemoveBufHidden", {}),
|
|
buffer = vim.api.nvim_get_current_buf(),
|
|
callback = function()
|
|
vim.bo.bufhidden = ""
|
|
end,
|
|
once = true,
|
|
})
|
|
end
|
|
view.focus()
|
|
end
|
|
|
|
local function get_target_winid(mode)
|
|
local target_winid
|
|
if not M.window_picker.enable or mode == "edit_no_picker" or mode == "preview_no_picker" then
|
|
target_winid = lib.target_winid
|
|
|
|
-- first available window
|
|
if not vim.tbl_contains(vim.api.nvim_tabpage_list_wins(0), target_winid) then
|
|
target_winid = first_win_id()
|
|
end
|
|
else
|
|
-- pick a window
|
|
if type(M.window_picker.picker) == "function" then
|
|
target_winid = M.window_picker.picker()
|
|
else
|
|
target_winid = pick_win_id()
|
|
end
|
|
if target_winid == nil then
|
|
-- pick failed/cancelled
|
|
return
|
|
end
|
|
end
|
|
|
|
if target_winid == -1 then
|
|
target_winid = lib.target_winid
|
|
end
|
|
return target_winid
|
|
end
|
|
|
|
-- This is only to avoid the BufEnter for nvim-tree to trigger
|
|
-- which would cause find-file to run on an invalid file.
|
|
local function set_current_win_no_autocmd(winid, autocmd)
|
|
local eventignore = vim.opt.eventignore:get()
|
|
vim.opt.eventignore:append(autocmd)
|
|
vim.api.nvim_set_current_win(winid)
|
|
vim.opt.eventignore = eventignore
|
|
end
|
|
|
|
local function open_in_new_window(filename, mode)
|
|
if type(mode) ~= "string" then
|
|
mode = ""
|
|
end
|
|
|
|
local target_winid = get_target_winid(mode)
|
|
if not target_winid then
|
|
return
|
|
end
|
|
|
|
-- non-floating, non-nvim-tree windows
|
|
local win_ids = vim.tbl_filter(function(id)
|
|
local config = vim.api.nvim_win_get_config(id)
|
|
local bufnr = vim.api.nvim_win_get_buf(id)
|
|
return config and config.relative == "" or utils.is_nvim_tree_buf(bufnr)
|
|
end, vim.api.nvim_list_wins())
|
|
|
|
local create_new_window = #win_ids == 1 -- This implies that the nvim-tree window is the only one
|
|
local new_window_side = (view.View.side == "right") and "aboveleft" or "belowright"
|
|
|
|
-- Target is invalid: create new window
|
|
if not vim.tbl_contains(win_ids, target_winid) then
|
|
vim.cmd(new_window_side .. " vsplit")
|
|
target_winid = vim.api.nvim_get_current_win()
|
|
lib.target_winid = target_winid
|
|
|
|
-- No need to split, as we created a new window.
|
|
create_new_window = false
|
|
if mode:match "split$" then
|
|
mode = "edit"
|
|
end
|
|
elseif not vim.o.hidden then
|
|
-- If `hidden` is not enabled, check if buffer in target window is
|
|
-- modified, and create new split if it is.
|
|
local target_bufid = vim.api.nvim_win_get_buf(target_winid)
|
|
if vim.api.nvim_buf_get_option(target_bufid, "modified") then
|
|
if not mode:match "split$" then
|
|
mode = "vsplit"
|
|
end
|
|
end
|
|
end
|
|
|
|
local fname = vim.fn.fnameescape(filename)
|
|
fname = utils.escape_special_chars(fname)
|
|
|
|
local cmd
|
|
if create_new_window then
|
|
cmd = string.format("%s vsplit %s", new_window_side, fname)
|
|
elseif mode:match "split$" then
|
|
cmd = string.format("%s %s", mode, fname)
|
|
else
|
|
cmd = string.format("edit %s", fname)
|
|
end
|
|
|
|
if (mode == "preview" or mode == "preview_no_picker") and view.View.float.enable then
|
|
-- ignore "WinLeave" autocmd on preview
|
|
-- because the registered "WinLeave"
|
|
-- will kill the floating window immediately
|
|
set_current_win_no_autocmd(target_winid, { "WinLeave", "BufEnter" })
|
|
else
|
|
set_current_win_no_autocmd(target_winid, { "BufEnter" })
|
|
end
|
|
|
|
pcall(vim.cmd, cmd)
|
|
lib.set_target_win()
|
|
end
|
|
|
|
local function is_already_loaded(filename)
|
|
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_is_loaded(buf_id) and filename == vim.api.nvim_buf_get_name(buf_id) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function edit_in_current_buf(filename)
|
|
require("nvim-tree.view").abandon_current_window()
|
|
vim.cmd("keepalt keepjumps edit " .. vim.fn.fnameescape(filename))
|
|
end
|
|
|
|
function M.fn(mode, filename)
|
|
if type(mode) ~= "string" then
|
|
mode = ""
|
|
end
|
|
|
|
if mode == "tabnew" then
|
|
return open_file_in_tab(filename)
|
|
end
|
|
|
|
if mode == "drop" then
|
|
return drop(filename)
|
|
end
|
|
|
|
if mode == "tab_drop" then
|
|
return tab_drop(filename)
|
|
end
|
|
|
|
if mode == "edit_in_place" then
|
|
return edit_in_current_buf(filename)
|
|
end
|
|
|
|
local buf_loaded = is_already_loaded(filename)
|
|
|
|
local found_win = utils.get_win_buf_from_path(filename)
|
|
if found_win and (mode == "preview" or mode == "preview_no_picker") then
|
|
return
|
|
end
|
|
|
|
if not found_win then
|
|
open_in_new_window(filename, mode)
|
|
else
|
|
vim.api.nvim_set_current_win(found_win)
|
|
vim.bo.bufhidden = ""
|
|
end
|
|
|
|
if M.resize_window then
|
|
view.resize()
|
|
end
|
|
|
|
if mode == "preview" or mode == "preview_no_picker" then
|
|
return on_preview(buf_loaded)
|
|
end
|
|
|
|
if M.quit_on_open then
|
|
view.close()
|
|
end
|
|
end
|
|
|
|
function M.setup(opts)
|
|
M.quit_on_open = opts.actions.open_file.quit_on_open
|
|
M.resize_window = opts.actions.open_file.resize_window
|
|
if opts.actions.open_file.window_picker.chars then
|
|
opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper()
|
|
end
|
|
M.window_picker = opts.actions.open_file.window_picker
|
|
end
|
|
|
|
return M
|