Skip to content

feat(watch): implement --debounce flag with duration#16187

Merged
Bahex merged 11 commits intonushell:mainfrom
lucascherzer:feat/watch-duration
Jul 29, 2025
Merged

feat(watch): implement --debounce flag with duration#16187
Bahex merged 11 commits intonushell:mainfrom
lucascherzer:feat/watch-duration

Conversation

@lucascherzer
Copy link
Copy Markdown
Contributor

@lucascherzer lucascherzer commented Jul 16, 2025

Fixes #16178

Description

This PR implement --debounce flag with duration for watch.

This new flag has a weird issue with calculating the durations.

When I timed it with a debounce time of 1min, it registered the write at 1m11s...

User-Facing Changes

Release notes summary

New watch --debounce option

The watch command now has --debounce flag, which takes a duration value. This will replace the --debounce-ms flag which takes an int rather than a duration, and will eventually take over its -d short flag.

Deprecate watch --debounce-ms

TODO(release-notes): Move this to Deprecations section
TODO(release-notes): verify link works after generating ToC

With the new watch --debounce option, the --debounce-ms option is no longer necessary. Use watch --debounce with a duration value instead.

Tests + Formatting

  • 🟢 toolkit fmt
  • 🟢 toolkit clippy
  • 🟢 toolkit test
  • 🟢 toolkit test stdlib

After Submitting

Update the online docs for watch:

  • new flag --debounce: duration
  • -d is now the shorthand of --debounce
  • --debounce-ms will be deprecated

This new flag has a weird issue with calculating the durations.

When I timed it with a debounce time of 1min, it registered the write at
1m11s...
@lucascherzer
Copy link
Copy Markdown
Contributor Author

Testing this again after the short change, when I make the debounce time 1min, it registers the write after 1m5s.

@lucascherzer
Copy link
Copy Markdown
Contributor Author

After investigating this, it seems to be an issue with the crate that does the inode watching itself (notify_debouncer_full).

It does not seem to use hooks for file changes and instead monitors them at regular intervals.
These intervals are defined as 1/4 * debounce_time [1], meaning the 5 second and 11 second delays I noticed were within this check interval of 15sec given the debounce time of 1 minute and thus, to be expected.

[1] https://docs.rs/notify-debouncer-full/latest/notify_debouncer_full/fn.new_debouncer.html

@lucascherzer lucascherzer marked this pull request as ready for review July 16, 2025 16:59
@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented Jul 16, 2025

  1. instead of deprecating non-duration times, you could make the signature a SyntaxShape::OneOf(SyntaxShape::Int, SyntaxShape::Duration). This avoids a breaking change while still allowing both scenarios.
  2. If the current crate isn't good enough, we could explore exchanging it for a better one that works better or is better supported, assuming it works cross platform.

@lucascherzer
Copy link
Copy Markdown
Contributor Author

lucascherzer commented Jul 16, 2025

Regarding 1
we discussed deprecation in #16178. The existing flag is named debounce-ms. If that were to accept both Int and Duration, I would find that very confusing given its suffix.

Regarding 2
In my opinion, it works well enough in this use case. Though we should maybe discuss this in a dedicated issue/discussion. I can imagine there are scenarios where this might not be acceptable for users.

@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented Jul 16, 2025

ya, debounce-ms wouldn't make sense for other duration units

Copy link
Copy Markdown
Member

@sholderbach sholderbach left a comment

Choose a reason for hiding this comment

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

Thanks for tackling this so quickly, looks pretty much as expected!

Comment on lines +126 to +135
debounce_duration = match u64::try_from(val.item) {
Ok(val) => Duration::from_millis(val),
Err(_) => {
return Err(ShellError::TypeMismatch {
err_message: "Debounce duration is invalid".to_string(),
span: val.span,
});
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pinging @132ikl on how we now raise the deprecation warning correctly since #16147

The ShellError::DeprecationWarning still exists but I guess that is not the way to go anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

if you are referring to the second error, it is actually a type mismatch. This condition occurs when using --debounce-ms with an arg that can not be converted to a u64.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In the Ok case we would want to send the deprecation warning that you use the old style flag. (at least that's how we did handled that so far)

Copy link
Copy Markdown
Contributor Author

@lucascherzer lucascherzer Jul 17, 2025

Choose a reason for hiding this comment

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

You mean stop execution and exit with the warning? So that the old flag is never actually executed?
I.e.

Ok(_) => return Err(ShellWarning::Deprecated ...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No still running but reporting the warning before returning the old result. I think that mechanism was revised with #16147 (before you would run report_error or similar with a ShellError::DeprecationWarning.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good! I implemented your feedback

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, this should be handled via a DeprecationEntry. You don't need to do anything special at run-time, just handle it the same way as before (maybe add a comment noting to remove the section of code once we remove the deprecated option)

Here's an example of how you can set this up, you should add a fn deprecation_info to the impl Command for Watch and then use DeprecationType::Flag. Also we generally do two releases out for deprecations, so you can set since to 0.106.0 and expected_removal to 0.108.0. Let me know if you have any issues!

internal_span: Span,
},
Duration {
/// The duration in nanoseconds.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

good catch, wouldn't have guessed that that was not documented here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was confused initially. Thought it'd be milliseconds

@@ -120,7 +121,18 @@ impl Command for Watch {
}
(None, Some(val)) => {
debounce_duration = match u64::try_from(val.item) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This I was not sure about. If someone can tell me what its supposed to say, I can adjust it. We should then document this as well

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

good point, I didn't even think to document the new deprecation process. I can add something to devdocs when I get a chance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this correct, or do I have to change something here?

Copy link
Copy Markdown
Contributor Author

@lucascherzer lucascherzer left a comment

Choose a reason for hiding this comment

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

@132ikl is this what you meant?

@lucascherzer
Copy link
Copy Markdown
Contributor Author

I'll have a look at the failed CI tomorrow

@132ikl
Copy link
Copy Markdown
Member

132ikl commented Jul 17, 2025

Yes, the deprecation_info looks good. You don't need the report_shell_error stuff when using deprecation_info, feel free to just remove that

Copy link
Copy Markdown
Member

@132ikl 132ikl left a comment

Choose a reason for hiding this comment

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

one small thing, otherwise looks good to me 😄

Comment on lines +146 to +161
(Some(v), None) => match v.item {
dur @ Value::Duration { val, .. } => {
debounce_duration = match u64::try_from(val) {
Ok(d) => Duration::from_nanos(d),
Err(_) => {
return Err(ShellError::TypeMismatch {
err_message: "Debounce duration is invalid".to_string(),
span: dur.span(),
});
}
};
}
// This should be unreachable, as the parser will fail
// before this is ever called. I will leave it in anyways
// so we can handle it gracefully once it never arrives
any => {
Copy link
Copy Markdown
Member

@132ikl 132ikl Jul 18, 2025

Choose a reason for hiding this comment

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

I think we can just do this here:

Suggested change
(Some(v), None) => match v.item {
dur @ Value::Duration { val, .. } => {
debounce_duration = match u64::try_from(val) {
Ok(d) => Duration::from_nanos(d),
Err(_) => {
return Err(ShellError::TypeMismatch {
err_message: "Debounce duration is invalid".to_string(),
span: dur.span(),
});
}
};
}
// This should be unreachable, as the parser will fail
// before this is ever called. I will leave it in anyways
// so we can handle it gracefully once it never arrives
any => {
(Some(v), None) => {
let Value::Duration { val, ..} = v.item else {
return Err(ShellError::TypeMismatch {
err_message: "Debounce duration must be a duration".to_string(),
span: v.span,
});
}
val

this suggestion won't apply cleanly since I can't do a suggestion over unchanged lines, but the any match branch would have to be manually removed as well

Copy link
Copy Markdown
Contributor Author

@lucascherzer lucascherzer Jul 21, 2025

Choose a reason for hiding this comment

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

fair, that is kind of repetitive. In c92711c, I removed the any branch and refactored this according to your suggestion. I also use v.item.span() instead of v.span due to the deprecation notice

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Semantically there is a difference between the Spanned.span returned from a CallExt/Call argument handler and the span inherent to the Value which you get by Value.span() (and is stored in internal_span.

The Spanned<T> should be filled with the span of the particular argument expression while the Value has its span pointing to its point of creation.

While this should be the same for a literal I think this would point to two different locations when you for example pass a variable to an argument

let foo = 42
bar --baz $foo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So do you want this to use v.span instead? What about the deprecation notice - how is this planned to go in the future?

@Bahex Bahex merged commit 459f3c0 into nushell:main Jul 29, 2025
16 checks passed
@github-actions github-actions bot added this to the v0.107.0 milestone Jul 29, 2025
@sholderbach sholderbach added the category:deprecation Related to the deprecation of commands/features/options label Jul 29, 2025
@sholderbach sholderbach added notes:breaking-changes This PR implies a change affecting users and has to be noted in the release notes notes:mention Include the release notes summary in the "Hall of Fame" section labels Jul 29, 2025
@sholderbach
Copy link
Copy Markdown
Member

Can you prepare a paragraph for the upcoming release notes mentioning what you have to going forward. Either by expanding User-facing Changes or directly in the release notes nushell/nushell.github.io#1999

@132ikl 132ikl removed the notes:mention Include the release notes summary in the "Hall of Fame" section label Aug 19, 2025
@132ikl 132ikl mentioned this pull request Aug 19, 2025
1 task
@132ikl 132ikl added notes:ready The "Release notes summary" section of this PR is ready to be included in our release notes. notes:additions Include the release notes summary in the "Additions" section and removed notes:breaking-changes This PR implies a change affecting users and has to be noted in the release notes labels Aug 19, 2025
sholderbach pushed a commit that referenced this pull request Aug 19, 2025
This PR reverts the breaking change of short flag change of `watch -d`
to `--debounce` instead of `--debounce-ms`. This fully prevents #16187
from being a breaking change.

Before #16187:
```nushell
watch -d 1000 foo {}
# => Now watching files at "/home/rose/foo". Press ctrl+c to abort.
```

Before this PR (after #16187):
```nushell
watch -d 1000 foo {}
# => Error: nu::parser::parse_mismatch
# => 
# =>   × Parse mismatch during operation.
# =>    ╭─[entry #15:1:10]
# =>  1 │ watch -d 1000 foo {}
# =>    ·          ──┬─
# =>    ·            ╰── expected duration with valid units
# =>    ╰────
```

After this PR (after #16187):
```nushell
watch -d 1000 foo {}
# => Warning: nu::parser::deprecated
# => 
# =>   ⚠ Flag deprecated.
# =>    ╭─[entry #3:1:7]
# =>  1 │ watch -d 1000 foo {}
# =>    ·       ─┬
# =>    ·        ╰── watch --debounce-ms was deprecated in 0.107.0 and will be removed in 0.109.0.
# =>    ╰────
# =>   help: `--debounce-ms` will be removed in favour of  `--debounce`
# => 
# => Now watching files at "/home/rose/foo". Press ctrl+c to abort.
```

This PR also fixes the `DeprecationEntry` which incorrectly had a `--`
in it, which I failed to realize when reviewing #16187. We should add a
`debug_assert` or something for this.

Rel: #16187

## Release notes summary - What our users need to know

N/A

## Tasks after submitting

- [ ] Add `debug_assert` for `DeprecationType::Flag`s with `--`
@lucascherzer
Copy link
Copy Markdown
Contributor Author

Can you prepare a paragraph for the upcoming release notes mentioning what you have to going forward. Either by expanding User-facing Changes or directly in the release notes nushell/nushell.github.io#1999

Sorry, I overlooked this. Does this still need to be done?

@132ikl
Copy link
Copy Markdown
Member

132ikl commented Sep 2, 2025

no worries, I added a release notes summary for the PR already

@lucascherzer
Copy link
Copy Markdown
Contributor Author

thanks :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category:deprecation Related to the deprecation of commands/features/options notes:additions Include the release notes summary in the "Additions" section notes:ready The "Release notes summary" section of this PR is ready to be included in our release notes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use duration for --debounce-ms in watch

5 participants