Skip to content

Latest commit

 

History

History
464 lines (328 loc) · 15.5 KB

MIGRATION-v4.md

File metadata and controls

464 lines (328 loc) · 15.5 KB

Try Dev Branch

Setup Lazy for Devs

Go to the dev directory you specify to lazy.nvim.

git clone git@github.com:pysan3/neo-tree.nvim.git
cd ./neo-tree.nvim
git checkout -t origin/v4-dev

You cannot have dev tree installed alongside neo-tree, and you cannot access neo-tree while developing. That's a bad thing, so let's make it so we can switch which neo-tree to use via an environment variable.

With this hack and something like tmux, run nvim in one pane as usual to get good old neo-tree, and run NVIM_NEOTREE_DEV=1 nvim in another pane to launch a nvim instance with dev-tree installed side-by-side.

return {
  "nvim-neo-tree/neo-tree.nvim",
  dir = vim.env.NVIM_NEOTREE_DEV and "/path/to/neo-tree.nvim" or nil, -- Add this line and point to the cloned repo.
  version = false,
  dependencies = {
    { "MunifTanjim/nui.nvim" },
    { "3rd/image.nvim" },
    { "pysan3/pathlib.nvim" },
    { "nvim-neotest/nvim-nio" },
    { "nvim-tree/nvim-web-devicons" },
    { "miversen33/netman.nvim" },
  },
  opts = {
    -- ...
  }
}

Rest of your config should not have any breaking changes, except that I haven't implemented the complete feature set yet, so some options are just ignored now.

Road Map

Here are the list of features that I haven't implemented / tested yet. I'll mostly work from top to bottom, but I may skip one or another based on my interests haha.

🔳 Command Parser Autoload

Make auto completion possible with :Neotree command. This one is pretty difficult as results must be returned before lazy loading.

🔳 Command Args

  • action
    • "close"
    • "focus"
    • "show"
    • "toggle"
    • "closeall"
    • "toggleall"
  • source
    • filetree
    • filesystem
    • buffers
    • git
    • document symbols
  • position
    • left, right, top, bottom
    • float
    • current
  • toggle
  • scope
    • global
    • tabpage
    • window
  • reveal
  • reveal_file
    • reveal file is outside of cwd
  • dir
    • ask if change cwd
  • id

✅ Neotree float

I just haven't looked into the nui options.

  • Implement wm.create_win.
  • Set window color groups here.
  • Close when not in focus.

🔳 Sort Nodes

I want to come up with a more efficient way of sorting the nodes. v3 does a deep sort for every render, but if we can keep track of what was added, theoretically we only need to sort the modified nodes.

✅ Highlights

Least priority for me sadly. I'm pretty sure old code will just work as is.

  • existing code worked as-is!!

✅ Steal Prevention

This is my next big thing to tackle.

  • send current buffer to previous non neo-tree window.
    • autocmd for each manager.
    • update jump info.
    • remember previous window on WinLeave

✅ Cursor Position Save

  • when curpos is saved
    • close
      • no need to do this anymore? bufdelete should handle this
    • before render_tree
    • follow_internal
      • instead use focus_node
    • bufdelete
      • use BufWinLeave instead
  • when restored
    • after render_tree

🔳 More Position Work

Pre-alpha.

Current code is very hacky.

Implement some kind of session save / restore mechanism that is able to survive across different nvim sessions.

We now have an upstream issue that nvim_buf_set_lines (used inside NuiTree:render()) changes the cursor position. My version of renderer.position.restore() does force the cursor to the correct position but there's a noticeable flash before restore kicks in.

✅ Keybinds

Keybind Commands

  • rewrite common/commands
    • i: show_file_details
    • o: show_help
      • oc: order_by_created
      • od: order_by_diagnostics
      • ot: order_by_type
      • og: order_by_git_status
      • om: order_by_modified
      • os: order_by_size
    • <esc>: cancel
    • >: next_source
    • <: prev_source
    • ?: show_help
    • e: toggle_auto_expand_width
    • P: toggle_preview
    • q: close_window
    • C: close_node
    • z: close_all_nodes
  • rewrite filetree/commands.
    • filesystem operations
      • add
        • focus added file
      • open
      • delete
        • update tree
      • move
      • copy
      • open with window_picker
    • clipboard operations
    • search operations
    • git operations
      • prev/next git modified
    • node operations
      • toggle
      • toggle hidden
      • toggle gitignored
    • tree operations
      • set cwd
      • move up tree

✅ Search

Implement a unified API to be able to search through the nodes of the tree. Maybe copy all nodes into a self.search_tree for performance?

  • self:prepare_search_tree(search_term: string)
    • returns a new NuiTree?
    • Should I depend on external binaries here?
  • Redraw
  • default sort when adding nodes
  • self.overwrite_sort_function
    • instead keep track of previous algorithm
    • search score
    • order_by_*
  • self:sort_tree(algorithm_name: string, sort_function: fun(a: node, b: node): boolean)
    • do nothing when name is the same as previous sort
  • tests

Order By...

  • Work on order_by_* commands.
  • Reimplement help page.
    • Rewrite the whole thing cuz the current implementation is a mess.

🔳 Event Handlers

None of them are correctly triggered, and the API might change in the future.

  • follow current file
  • change cwd

🔳 Global Config Value Access

Current code tries to access user config with require("neo-tree").config from random places. This is not a good design.

I've added the following label to places that I must refactor later. Currently, I simply use the default config as a workaround.

-- TODO: We cannot fetch global config options here. Needs refactor or input with func args.

✅ File Watcher

  • Detect and update tree on file change.
  • fix: file watcher is registered more than once.
  • Use debounce based on num of waiting files.
  • Scan check if dir is already scanned.

🔳 Git Watcher

  • Use nio.process to capture git status instead.
    • Done on pathlib side.
  • Incremental update when done.
  • Debounce.
    • Done on pathlib side.
  • watch with fs_watch
  • watch with tree update (for users without fs_watch)
    • BufWritePost

🔳 renderer.redraw

  • Halt current rendering and restart (call state:redraw)
  • But not when state is hidden.
  • But redraw when state regains focus.

✅ Tab Sync

On tab switch, recreate the other layouts.

Does not work when left -> right -> top -> right. Do not have any way to test it with code. (Requires human intervention). Maybe create autocmd with AuG ID? create_aug is not updated.

Solved!!!

🔳 GC old state

Especially window-scoped states when reference is done. Call state:free.

Breaking Changes

Misc

common.commands.paste_from_clipboard

(aka cc.paste_from_clipboard)

  • callback was given dest_folder: NuiTreeNode, destination: string and called for each new file.
  • callback is given a list of destinations dest_folder: NuiTreeNode, destinations: PathlibPath[] and called only once.

Manager

The biggest rewrite happens at manager.

Previously it was "neo-tree.sources.manager" which I've moved it to "neo-tree.manager.init". The old manager was to initialize a state and to fetch the current active state in a very hacky code. In my rewrite, the manager strongly holds references to all states and is the module that is responsible of deciding which state to use and switch to the appropriate state for each :Neotree xxx call.

A new instance of manager is created for each tabpage. This brings a lot of merits, that what state is shown where (left/right/top/left) can be managed in each manager and separately for each tabpage.

However, a globally shared table manager.source_lookup is set as a class property, meaning that all managers access the same table so that the State Instances can be shared across all tabpages.

Share State Among Tabs

I'd like to add a global option config.share_state_among_tabs that, when set to true, all tabs will have the exact same neo-tree layouts.

This is very easy. Add a TabEntered autocmd for each manager, and when the active tab is what you are supposed to handle, reference global_position_state and rearrange the layout to match this table.

When a state is opened or closed, submit that to the global_position_state so that when user switches to a different tab, your layout is copied to them.

Sources

Buffer / Window Management

As Manager handles windows and buffers, each source / state does not need to know when / where it is being placed.

Instead, it is told only about the window width, and whether source is allowed to request an expansion of the width.

After state has finished tree:render(), it does not need to check whether a window is valid or acquire_window or anything like that but just needs to call manager:done().

Therefore, state will no longer have tabid, winid, bufnr attributes. state.current_position is kept (and updated correctly) for backwards compatibility, but it is advised not to rely on the value of this attribute.

State Instance

WIP

External Sources

There are no specific changes regarding external sources. See Sources for the list you changes you need.

However, you have the ability to specify your own commands for users to set for keybinds.

Nio Async vs Callbacks

Lua heavily uses the callback method to make the execution somewhat async. However, this adds more complexity to the code and even worse, the base (parent) function cannot know when the callback has ended, nor get a return value from the function call.

This is where nvim-nio comes handy, but this is a neat wrapper around lua coroutines, so let's learn from the ground up.

Lua Async Await Article

FYI, https://github.com/ms-jpq/lua-async-await is the best article and shows how to implement such library yourself with great details. I really recommend reading it if you are interested.

How to Coroutine

Callbacks and Goal

I'll try to explain it with more examples and diagrams.

Let's assume we want to ask for a file name and create that file.

local function main_cb()
  vim.ui.input({ prompt = "New file: "}, function (value)
    vim.loop.fs_open(value, "w", 420, function (fd)
      assert(fd, "Could not open " .. value)
      vim.loop.fs_close(fd)
    end)
  end)
end

Our goal is to write something like this.

local function main_async()
  local value = vim.ui.input({ prompt = "New file: "})
  local fd = vim.loop.fs_open(value, "w", 420)
  assert(fd, "Could not open " .. value)
  vim.loop.fs_close(fd)
end

Lua Coroutines

If you don't know anything about coroutine, read the Lua Coroutine Doc, There are 2 important functions here: coroutine.resume and coroutine.yield.

coroutine.resume will block the execution of one thread until coroutine.yield is called in another thread and when you pass arguments to yield, those will be passed over to resume.

Using this trick, the main thread (main_async) can wait until vim.ui.input gets a user input and the child thread (where vim.ui.input runs) can yield back the input on_confirm.

| main thread                | sub thread                  |

| main_async
|       |
| coroutine.resume --------- > vim.ui.input
|       |                           |
|       |                      on_confirm
|       |                           |
|       |                      coroutine.yield(user_input)
|       | < ------------------------
|       V
| coroutine.resume !!
| local value = <result-from-yield>

Basically, we will use coroutine.resume(vim.ui.input, { prompt = ... }, <callback>) and specify a callback that calls coroutine.yield at the end to return the result back to resume.

Again https://github.com/ms-jpq/lua-async-await explains to more extent such as how to handle nested coroutines (i.e. sub thread also calls resume and wait for a sub-sub thread) and add error handling, proper traceback support on top of this mechanism.

Create an Async Function

nvim-nio provides building blocks to implement this coroutine system with nio.wrap.

When the last argument is the callback, it is as simple as this...

local nio = require("nio")

local num_args = 2              -- how many arguments `vim.ui.input` expects, including the cb.
local opt = { strict = true }   -- if strict, wrapped function raises exception when called in the _main thread_.
local async_input = nio.wrap(vim.ui.input, num_args, opt)

nio.run(function ()
  local value = async_input({ prompt = "..." })
end)

Let's also make one for vim.loop.fs_open.

local nio = require("nio")

local async_fs_open = nio.wrap(vim.loop.fs_open, 3, {})

Remember that the callback must be the last argument, so you'll need to create a temporary wrapper function if that's not the case. One example is vim.defer_fn(cb, ms) which is explained as an example in nio's readme.

Summary

Using these building blocks, the first example can be implemented like this.

local nio = require("nio")

local async_fs_close = nio.wrap(vim.loop.fs_close, 2, {})
local function main_async()
  local value = async_input({ prompt = "New file: "})
  local fd = async_fs_open(value, "w", 420)
  assert(fd, "Could not open " .. value)
  async_fs_close(fd)
end

nio.run(main_async)

Well, Actually...

Well, actually these functions are already provided by nio with

So the actual result will be

local nio = require("nio")

local function main_easy()
  local value = nio.ui.input({ prompt = "New file: "})
  local fd = nio.file.open(value, "w", 420)
  assert(fd, "Could not open " .. value)
  nio.uv.fs_close(fd)

  -- and you can add more code like
  vim.schedule(function()
    vim.cmd.edit(value)
  end)
end

nio.run(main_easy)

The biggest benefit is that although the code looks very much like regular code, it never blocks the execution of the main neovim lua runtime (except inside vim.schedule ofc), and user will never experience any stutter or freeze in the main UI.