-
-
Notifications
You must be signed in to change notification settings - Fork 624
feat: async rename and create file #1870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
cc5eda6
to
ec32ac2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
ec32ac2
to
965e284
Compare
Thanks for the updates @yioneko This PR deserves significant review time which I can allocate this weeken. |
Just post the discussion about async lib in core: neovim/neovim#19624. My current implementation is quite minimal so it might be fairly convenient to migrate to it once standardized. Also, it's reasonable to suspend this pr unless the async lib is implemented in core, and we could use it directly. Open to any suggestions. |
It sounds a core implementation is quite far away. I'm quite happy to have the implementation in nvim-tree and migrate later, which should be trivial. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to run with this; experimental features are... experimental.
Please:
- Revert :help formatting changes
- Remove API
- Revert unnecessary events changes
- Revert bulk move
- Revert open file changes
- Normalise create-file - DRY
-
Force and assume filesystem_watchers.enable - Use "standard" windows feature flag detection: feat: async rename and create file #1870 (comment)
- More useful error messages: feat: async rename and create file #1870 (comment)
@@ -1134,6 +1140,21 @@ Configuration for diagnostic logging. | |||
|nvim-tree.filesystem_watchers| processing, verbose. | |||
Type: `boolean`, Default: `false` | |||
|
|||
*nvim-tree.experimental* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO @alex-courtis expand on these and think about how to present them in the readme/wiki etc.
@yioneko I just discovered that review comments do not get posted until the review is complete. Please accept my apologies; I assumed you were able to see my comments from ~weeks ago. |
@alex-courtis Uhh, that's fine, I'm also busy during this period. Thanks for the detailed comments, I'll work on this next week. |
No worries. This is valuable functionality, but there is no rush. |
35c0b43
to
e8f7a42
Compare
e8f7a42
to
6e7f5ea
Compare
lua/nvim-tree/async.lua
Outdated
|
||
M.Interrupter = Interrupter | ||
|
||
---This is useful for cancelling execution async function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more pending question to make this ready for merge: should we implement cancellation of these operations? This is impossible in sync version but achievable now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YAGNI.
We can add cancellation when we have a use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good; we will get there.
Please:
- remove unused codepaths
- resolve my naive questions around the need to schedule
- don't use async prompts
lua/nvim-tree/async.lua
Outdated
|
||
M.Interrupter = Interrupter | ||
|
||
---This is useful for cancelling execution async function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YAGNI.
We can add cancellation when we have a use case.
end) | ||
end | ||
|
||
---Asynchronous `vim.schedule` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to do this? Please add comments here and at call sites as to why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As documented in :h lua-loop-callbacks
, we cannot call some vim.api.*
and all vim.fn.*
in luv callbacks. So every time before these calls, we should manually call vim.schedule
(async.schedule
here to avoid callback hells). It's my fault to assume that we already get this; I'll add necessary comments about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. So this schedule is essentially a thread join.
I do not understand the timing; surely it should be called immediately after the .call
or during the callback. It's called once at the start of create_file
and once at the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly we could do that for every callback, but usually this is only needed when the callback contains vim apis (or we are uncertain about this, such as user provided callback). In some cases we may not want to use vim.schedule
to delay the execution of callback like a sequence of consecutive luv calls and always delaying may have potential negative impact on performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. In that case, please move the calls to schedule immediately after the calls that require this join. It should be clear exactly when and where it is required.
local prompt_select = "Overwrite " .. new_file_path .. " ?" | ||
local prompt_input = prompt_select .. " y/n: " | ||
if M.enable_async then | ||
local item_short = async.call(lib.prompt, prompt_input, prompt_select, { "y", "n" }, { "Yes", "No" }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to make the prompt async? Only create_and_notify
etc. needs to be async.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is mandatory in async version. In fact, lib.prompt
itself is already async because of usage of callback, the only difference is that the async context of lib.prompt
is not controlled. If we use callback instead of wrapping with async.call
, create_and_notify
may not be called in the same async context as where async.exec
begins and the immediate following async.call
in create_and_notify
will throw error.
You can test it with overriding vim.ui.input
with https://github.com/stevearc/dressing.nvim or even a simple delayer:
local original = vim.ui.input
vim.ui.input = function(opts, on_confirm)
original(opts, function(input)
vim.defer_fn(function() on_confirm(input) end, 0)
end)
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In principle, all callback-style functions should be wrapped to make the async context consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately I still don't understand. Yes, the vim.ui.select calls themselves are asynchronous, however the callbacks appear to be executed on the main thread / context.
function M.prompt(prompt_input, prompt_select, items_short, items_long, callback)
...
if M.select_prompts then
vim.ui.select(items_short, { prompt = prompt_select, format_item = format_item }, function(item_short)
log.line("dev", "prompt select in_fast_event=%s is_thread=%s", vim.in_fast_event(), vim.is_thread())
callback(item_short)
end)
else
vim.ui.input({ prompt = prompt_input }, function(item_short)
log.line("dev", "prompt input in_fast_event=%s is_thread=%s", vim.in_fast_event(), vim.is_thread())
callback(item_short)
end)
end
end
[2023-01-23 10:29:44] [dev] prompt input in_fast_event=false is_thread=false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because it might be called in main thread (not in async thread), we need to use async.call
to transfer it to async thread, otherwise create_and_notify
will throw error because async.call
uses coroutine.yield
under the hood.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async.exec
is not required to be called from main thread. It's possible to create new coroutine in another coroutine. The coroutine (or thread, here) is just stackful statemachine and could be created anywhere.
Would you mind I make a simple guidance for understanding of current basic async implementation? I've seen that there are plenty of misunderstandings between us and possibly with future contributors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes please. A detailed explaination would be gratefully appreciated.
The outcome of this PR:
- file system operations for create / rename are asynchronous
- code changes are minimal and clear
- asynchronous operations are localised i.e. not spread over multiple callbacks / functions
Unless there is a very good reason, inputs should not be asynchronous. This adds unnecessary complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for that I'm gonna to be off-topic, I think the referred async input codepath would not be ever reached, as we already checked the existence of file before async.exec
:
nvim-tree.lua/lua/nvim-tree/actions/fs/create-file.lua
Lines 136 to 142 in c71f47a
if utils.file_exists(new_file_path) then | |
notify.warn "Cannot create: file already exists" | |
return | |
end | |
if M.enable_async then | |
async.exec(create_file, new_file_path, function(err) |
Do you think it's safe to remove this? I just cp from the original implementation. There is no other place with async call to user input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see... that's a good catch!
It does look like this file_exits
will fail and exit early. The local function create_file(new_file_path)
call to file_exists
will always pass and the "Overwrite " .. new_file_path .. " ?"
prompt cannot occur.
This is existing behaviour: we can safely remove the file_exists
check in create_file
.
Please raise a separate PR to remove this dead code path. We will merge that first, keeping our changes small and atomic and easily revertable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found that I referred the wrong variable, new_file_path
-> path_to_create
, sorry about that. However, the truth that this is a dead code remains; and I think we should early return instead of prompting for overwrite for already existing file.
@@ -0,0 +1,180 @@ | |||
---Idea taken from: https://github.com/ms-jpq/lua-async-await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot of unused codepaths in here. Let's trim it down. If we do need functionality we can refer to the source.
It looks like:
in_async
wrap
all
interrupt/abort
Any updates on this one @yioneko ? We are getting very close... |
Closing due to inactivity. The concept and implementation of async is sound; changes to the usages are required. |
Split of #1863.
New experimental options:
I select these two operations because their async version is nearly 1to1 copy of the original implementation, and the risk of breaking is bit lower. Also, I think we could turn on
rename_file
by default, and let users choose to enablecreate_file
either if they feel well about the new async behavior.