Skip to content

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

Closed
wants to merge 3 commits into from

Conversation

yioneko
Copy link
Contributor

@yioneko yioneko commented Dec 31, 2022

Split of #1863.

New experimental options:

  experimental = {
    async = {
      create_file = false,
      rename_file = false,
    },
  },

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 enable create_file either if they feel well about the new async behavior.

@yioneko yioneko changed the title faet: async rename and create file feat: async rename and create file Dec 31, 2022
@yioneko yioneko force-pushed the feat-async-fs-op-s1 branch from cc5eda6 to ec32ac2 Compare December 31, 2022 02:18
Copy link
Collaborator

@gegoune gegoune left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work!

@yioneko yioneko force-pushed the feat-async-fs-op-s1 branch from ec32ac2 to 965e284 Compare December 31, 2022 03:37
@alex-courtis
Copy link
Member

Thanks for the updates @yioneko

This PR deserves significant review time which I can allocate this weeken.

@yioneko
Copy link
Contributor Author

yioneko commented Jan 5, 2023

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.

@alex-courtis
Copy link
Member

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.

@alex-courtis alex-courtis changed the base branch from master-reverting to master January 9, 2023 02:43
Copy link
Member

@alex-courtis alex-courtis left a 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:

@@ -1134,6 +1140,21 @@ Configuration for diagnostic logging.
|nvim-tree.filesystem_watchers| processing, verbose.
Type: `boolean`, Default: `false`

*nvim-tree.experimental*
Copy link
Member

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.

@alex-courtis
Copy link
Member

alex-courtis commented Jan 14, 2023

@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.

@yioneko
Copy link
Contributor Author

yioneko commented Jan 14, 2023

@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.

@alex-courtis
Copy link
Member

@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.

@yioneko yioneko force-pushed the feat-async-fs-op-s1 branch from 35c0b43 to e8f7a42 Compare January 20, 2023 02:51
@yioneko yioneko force-pushed the feat-async-fs-op-s1 branch from e8f7a42 to 6e7f5ea Compare January 20, 2023 02:56

M.Interrupter = Interrupter

---This is useful for cancelling execution async function
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

@alex-courtis alex-courtis left a 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


M.Interrupter = Interrupter

---This is useful for cancelling execution async function
Copy link
Member

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`
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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" })
Copy link
Member

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.

Copy link
Contributor Author

@yioneko yioneko Jan 22, 2023

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

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

@yioneko yioneko Jan 23, 2023

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:

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.

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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

@alex-courtis
Copy link
Member

Any updates on this one @yioneko ?

We are getting very close...

@alex-courtis
Copy link
Member

Closing due to inactivity.

The concept and implementation of async is sound; changes to the usages are required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants