Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ will be a new branch that you can opt into, when it is a good time for you.

See [What is a Breaking Change?](#what-is-a-breaking-change) for details.

See [Changelog
3.0](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Changelog#30) for
breaking changes and deprecations in 3.0.
See [Changelog 3.0](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Changelog#30)
for breaking changes and deprecations in 3.0.

### User Experience GOOD :slightly_smiling_face: :thumbsup:

Expand All @@ -47,11 +46,14 @@ should you!

- Neo-tree won't let other buffers take over its window.
- Neo-tree won't leave its window scrolled to the last line when there is plenty
of room to display the whole tree.
- Neo-tree does not need to be manually refreshed (set
`use_libuv_file_watcher=true`)
- Neo-tree can intelligently follow the current file (set
`follow_current_file.enabled=true`)
of room to display the whole tree.
- Neo-tree does not need to be manually refreshed
(set `use_libuv_file_watcher = true`)
- Neo-tree can intelligently follow the current file
(set `follow_current_file.enabled = true`)
- Neo-tree can sync its clipboard across multiple trees, either globally
(within the same Neovim instance) or universally (across all Neovim
instances). Try `clipboard.sync = "global" | "universal"`.
- Neo-tree is thoughtful about maintaining or setting focus on the right node
- Neo-tree windows in different tabs are completely separate
- `respect_gitignore` actually works!
Expand All @@ -72,15 +74,19 @@ utilities, such as scanning the filesystem.

There are also some optional plugins that work with Neo-tree:

- [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons.
- [antosha417/nvim-lsp-file-operations](https://github.com/antosha417/nvim-lsp-file-operations) for LSP-enhanced renames/etc.
- [folke/snacks.nvim](https://github.com/folke/snacks.nvim) for image previews, see Preview Mode section.
- [snacks.rename](https://github.com/folke/snacks.nvim/blob/main/docs/rename.md#neo-treenvim) can also work with
Neo-tree
- [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
for file icons.
- [antosha417/nvim-lsp-file-operations](https://github.com/antosha417/nvim-lsp-file-operations)
for LSP-enhanced renames/etc.
- [folke/snacks.nvim](https://github.com/folke/snacks.nvim) for image previews,
see Preview Mode section.
- [snacks.rename](https://github.com/folke/snacks.nvim/blob/main/docs/rename.md#neo-treenvim)
can also work with Neo-tree
- [3rd/image.nvim](https://github.com/3rd/image.nvim) for image previews.
- If both snacks.nvim and image.nvim are installed. Neo-tree currently will
try to preview with snacks.nvim first, then try image.nvim.
- [s1n7ax/nvim-window-picker](https://github.com/s1n7ax/nvim-window-picker) for `_with_window_picker` keymaps.
- If both snacks.nvim and image.nvim are installed. Neo-tree currently will try
to preview with snacks.nvim first, then try image.nvim.
- [s1n7ax/nvim-window-picker](https://github.com/s1n7ax/nvim-window-picker) for
`_with_window_picker` keymaps.


### mini.deps example:
Expand Down Expand Up @@ -272,6 +278,9 @@ vim.keymap.set("n", "<leader>e", "<Cmd>Neotree<CR>")
require("neo-tree").setup({
close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab
popup_border_style = "NC", -- or "" to use 'winborder' on Neovim v0.11+
clipboard = {
sync = "none", -- or "global"/"universal" to share a clipboard for each/all Neovim instance(s), respectively
},
enable_git_status = true,
enable_diagnostics = true,
open_files_do_not_replace_types = { "terminal", "trouble", "qf" }, -- when opening files, do not use windows containing these filetypes or buftypes
Expand Down
101 changes: 101 additions & 0 deletions doc/neo-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Configuration ............... |neo-tree-configuration|
Components and Renderers .. |neo-tree-renderers|
Buffer Variables .......... |neo-tree-buffer-variables|
Popups .................... |neo-tree-popups|
Clipboard ................... |neo-tree-clipboard|
Sync ...................... |neo-tree-clipboard-sync|
Other Sources ............... |neo-tree-sources|
Buffers ................... |neo-tree-buffers|
Git Status ................ |neo-tree-git-status-source|
Expand Down Expand Up @@ -1937,6 +1939,105 @@ state to a string. The colors of the popup border are controlled by the
highlight group.


CLIPBOARD *neo-tree-clipboard*

Neo-tree features a clipboard that you can copy, cut, and paste nodes with (by
default, with y, x, and p respectively). This is currently only available for the
filesystem source.

CLIPBOARD SYNC *neo-tree-clipboard-sync*

Neo-tree's clipboard can be synced globally (across all Neo-trees within the
same Neovim instance) or universally (across all Neo-trees on a computer). The
default is to not sync at all. To change this option, change the
`clipboard.sync` option (options are `"none"|"global"|"universal"`). The
universal sync option relies on a file located under `stdpath("state") ..
"/neo-tree.nvim/clipboards".` You can also implement your own backend and pass
it to that option as well:
>lua
---Code derived from require('neo-tree.clipboard.sync.base')

---@class neotree.clipboard.Backend
local Backend = {}

---A backend saves and loads clipboards to and from states.
---Returns nil if the backend couldn't be created properly.
---@return neotree.clipboard.Backend?
function Backend:new()
local backend = {}
setmetatable(backend, self)
self.__index = self
-- if not do_setup() then
-- return nil -- will default to no sync/backend
-- end
return backend
end

-- local function applicable(state)
-- return state.name == "filesystem"
-- end

---Saves a state's clipboard to the backend.
---Automatically called whenever a user changes a state's clipboard.
---Returns nil when the save is not applicable.
---@param state neotree.State
---@return boolean? success_or_noop
function Backend:save(state)
-- if not applicable(state) then
-- return nil -- nothing happens
-- end

-- local saved, err = save_clipboard_to_somewhere(state)
-- if not saved then
-- return false, err -- will error
-- end

-- on true, neo-tree will try Backend:load with all other states
-- return true
end

---Given a state, determines what clipboard (if any), should be loaded.
---Automatically called when other states' clipboards saved successfully.
---Returns nil if the clipboard should not be changed.
---@param state neotree.State
---@return neotree.clipboard.Contents? clipboard
---@return string? err
function Backend:load(state)
-- if not applicable(state) then
-- return nil -- nothing happens
-- end

-- local clipboard, err = load_clipboard_from_somewhere(state)
-- if err then
-- -- don't modify the clipboard and log an error
-- return nil, err
-- end

-- change the clipboard to the saved clipboard
-- return clipboard
end

require("neo-tree").setup({
clipboard = {
sync = Backend
}
})
<

Additionally, this helper method exists on `require('neo-tree.clipboard')`:

>lua
---Load saved clipboards into all states (except one, if provided).
---@param exclude_state neotree.State?
function M.update_states(exclude_state)
-- ...
end
<

This method may be useful to call if a new clipboard exists but save hasn't
updated yet. For example, the universal backend calls this method when a
different instance of Neovim updates a clipboard file.

================================================================================
OTHER SOURCES ~
================================================================================
Expand Down
111 changes: 111 additions & 0 deletions lua/neo-tree/clipboard/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
local events = require("neo-tree.events")
local manager = require("neo-tree.sources.manager")
local log = require("neo-tree.log")
local renderer = require("neo-tree.ui.renderer")

local M = {}

---@class neotree.clipboard.Node
---@field action string
---@field node NuiTree.Node

---@alias neotree.clipboard.Contents table<string, neotree.clipboard.Node?>

---@alias neotree.clipboard.BackendNames.Builtin
---|"none"
---|"global"
---|"universal"

---@type table<string, fun():neotree.clipboard.Backend>
local builtins = {
none = function()
return require("neo-tree.clipboard.sync.base")
end,
global = function()
return require("neo-tree.clipboard.sync.global")
end,
universal = function()
return require("neo-tree.clipboard.sync.universal")
end,
}

M.builtin_backends = builtins

---@alias neotree.Config.Clipboard.Sync neotree.clipboard.BackendNames.Builtin|neotree.clipboard.Backend

---@class (exact) neotree.Config.Clipboard
---@field sync neotree.Config.Clipboard.Sync?

---@param opts neotree.Config.Clipboard
M.setup = function(opts)
opts = opts or {}
opts.sync = opts.sync or "none"

---@type neotree.clipboard.Backend?
local selected_backend
if type(opts.sync) == "string" then
local lazy_loaded_backend = M.builtin_backends[opts.sync]
if lazy_loaded_backend then
---@cast lazy_loaded_backend fun():neotree.clipboard.Backend
selected_backend = lazy_loaded_backend()
end
elseif type(opts.sync) == "table" then
local sync = opts.sync
---@cast sync -neotree.clipboard.BackendNames.Builtin
selected_backend = sync
end

if not selected_backend then
log.error("invalid clipboard sync method, disabling sync")
selected_backend = builtins.none()
end
M.current_backend = log.assert(selected_backend:new())
events.subscribe({
event = events.STATE_CREATED,
---@param new_state neotree.State
handler = function(new_state)
local clipboard, err = M.current_backend:load(new_state)
if not clipboard then
log.assert(not err, err)
return
end
new_state.clipboard = clipboard
end,
})

events.subscribe({
event = events.NEO_TREE_CLIPBOARD_CHANGED,
---@param state neotree.State
handler = function(state)
local ok, err = M.current_backend:save(state)
if ok == false then
log.error(err)
end
if ok then
M.update_states(state)
end
end,
})
end

---Load saved clipboards into all states (except one, if provided).
---@param exclude_state neotree.State?
function M.update_states(exclude_state)
-- try loading the changed clipboard into all other states
vim.schedule(function()
manager._for_each_state(nil, function(state)
if state == exclude_state then
return
end
local modified_clipboard, err = M.current_backend:load(state)
if not modified_clipboard then
log.assert(not err, err)
return
end
state.clipboard = modified_clipboard
renderer.redraw(state)
end)
end)
end

return M
61 changes: 61 additions & 0 deletions lua/neo-tree/clipboard/sync/base.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---@class neotree.clipboard.Backend
local Backend = {}

---A backend saves and loads clipboards to and from states.
---Returns nil if the backend couldn't be created properly.
---@return neotree.clipboard.Backend?
function Backend:new()
local backend = {}
setmetatable(backend, self)
self.__index = self
-- if not do_setup() then
-- return nil -- will default to no sync/backend
-- end
return backend
end

-- local function applicable(state)
-- return state.name == "filesystem"
-- end

---Saves a state's clipboard to the backend.
---Automatically called whenever a user changes a state's clipboard.
---Returns nil when the save is not applicable.
---@param state neotree.State
---@return boolean? success_or_noop
function Backend:save(state)
-- if not applicable(state) then
-- return nil -- nothing happens
-- end

-- local saved, err = save_clipboard_to_somewhere(state)
-- if not saved then
-- return false, err -- will error
-- end

-- on true, neo-tree will try Backend:load with all other states
-- return true
end

---Given a state, determines what clipboard (if any), should be loaded.
---Automatically called when other states' clipboards saved successfully.
---Returns nil if the clipboard should not be changed.
---@param state neotree.State
---@return neotree.clipboard.Contents? clipboard
---@return string? err
function Backend:load(state)
-- if not applicable(state) then
-- return nil -- nothing happens
-- end

-- local clipboard, err = load_clipboard_from_somewhere(state)
-- if err then
-- -- don't modify the clipboard and log an error
-- return nil, err
-- end

-- change the clipboard to the saved clipboard
-- return clipboard
end

return Backend
27 changes: 27 additions & 0 deletions lua/neo-tree/clipboard/sync/global.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local Backend = require("neo-tree.clipboard.sync.base")
---@class neotree.clipboard.GlobalBackend : neotree.clipboard.Backend
local GlobalBackend = Backend:new()

---@type table<string, neotree.clipboard.Contents?>

---@class neotree.clipboard.GlobalBackend
---@field clipboards table<string>
function GlobalBackend:new()
local backend = {}
setmetatable(backend, self)
self.__index = self

---@cast backend neotree.clipboard.GlobalBackend
backend.clipboards = {}
return backend
end

function GlobalBackend:save(state)
self.clipboards[state.name] = state.clipboard
end

function GlobalBackend:load(state)
return self.clipboards[state.name]
end

return GlobalBackend
Loading
Loading