Set Ready For Review
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Sorry for the delay reviewing this! This looks pretty good - just some small things.
if (!form_control_ || (deleted_count == 0 && inserted_count == 0)) {
nit: maybe add
```
DCHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
```
// Deletion step: remove text from change_offset to change_end .
// Endpoints that fall within the removed text (including exactly at its start
// or end) move to |change_offset|. Endpoints after the removed text shift
// left by |deleted_count|. Endpoints before the removed text are unchanged.
auto apply_delete = [&](unsigned pos) -> unsigned {
if (deleted_count == 0 || pos < change_offset) {
return pos;
}
return (pos <= change_end) ? change_offset : pos - deleted_count;
};
// Insertion step: insert |inserted_count| units at |change_offset|.
// Endpoints strictly after |change_offset| shift right by |inserted_count|.
// Endpoints at or before |change_offset| are unchanged.
auto apply_insert = [&](unsigned pos) -> unsigned {
if (inserted_count == 0 || pos <= change_offset) {
return pos;
}
return pos + inserted_count;
};
unsigned new_start = apply_insert(apply_delete(pre_start));
unsigned new_end = apply_insert(apply_delete(pre_end));
nit, I find this a little easier to read, and I believe it does the same thing:
```
auto calculate_new_offset = [&](unsigned pos) -> unsigned {
const unsigned change_end = change_offset + deleted_count;
// 1. Position is before the change; it's unaffected.
if (pos < change_offset) {
return pos;
}
// 2. Position is inside the deleted region; snap to the start.
if (pos <= change_end) {
return change_offset;
}
// 3. Position is after the change; shift by the net difference.
return pos - deleted_count + inserted_count;
};
unsigned new_start = calculate_new_offset(pre_start);
unsigned new_end = calculate_new_offset(pre_end);
```
CaptureFormControlRangePreEdit(static_cast<InputEvent&>(event));
Perhaps prefer `To<InputEvent>()` over static_cast.
if (RuntimeEnabledFeatures::FormControlRangeEnabled()) {
I think you always want to trace, not just when the feature is enabled.
for (wtf_size_t i = 0; i < form_control_ranges_.size(); ++i) {
if (form_control_ranges_[i] == range) {
form_control_ranges_.EraseAt(i);
break;
}
}
Perhaps
```
auto iter = std::ranges::find(form_control_ranges_, range);
if (iter != form_control_ranges_.end()) {
form_control_ranges_.erase(iter);
}
```
input_event.defaultPrevented() || form_control_ranges_.empty()) {
Does this ever get called if `defaultPrevented`? Seems like the only caller is in `DefaultEventHandler`, which shouldn't happen if the default was prevented. Perhaps you can just DCHECK?
const unsigned max_by_sel = old_length - selection_end;
Hmm, `max_by_sel` is a little hard to parse. Maybe `maximum_suffix_length`?
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
if (!form_control_ || (deleted_count == 0 && inserted_count == 0)) {
nit: maybe add
```
DCHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
```
Done
// Deletion step: remove text from change_offset to change_end .
// Endpoints that fall within the removed text (including exactly at its start
// or end) move to |change_offset|. Endpoints after the removed text shift
// left by |deleted_count|. Endpoints before the removed text are unchanged.
auto apply_delete = [&](unsigned pos) -> unsigned {
if (deleted_count == 0 || pos < change_offset) {
return pos;
}
return (pos <= change_end) ? change_offset : pos - deleted_count;
};
// Insertion step: insert |inserted_count| units at |change_offset|.
// Endpoints strictly after |change_offset| shift right by |inserted_count|.
// Endpoints at or before |change_offset| are unchanged.
auto apply_insert = [&](unsigned pos) -> unsigned {
if (inserted_count == 0 || pos <= change_offset) {
return pos;
}
return pos + inserted_count;
};
unsigned new_start = apply_insert(apply_delete(pre_start));
unsigned new_end = apply_insert(apply_delete(pre_end));
nit, I find this a little easier to read, and I believe it does the same thing:
```
auto calculate_new_offset = [&](unsigned pos) -> unsigned {
const unsigned change_end = change_offset + deleted_count;
// 1. Position is before the change; it's unaffected.
if (pos < change_offset) {
return pos;
}
// 2. Position is inside the deleted region; snap to the start.
if (pos <= change_end) {
return change_offset;
}
// 3. Position is after the change; shift by the net difference.
return pos - deleted_count + inserted_count;
};unsigned new_start = calculate_new_offset(pre_start);
unsigned new_end = calculate_new_offset(pre_end);
```
Thank you - that's so much better!
CaptureFormControlRangePreEdit(static_cast<InputEvent&>(event));
Perhaps prefer `To<InputEvent>()` over static_cast.
Ended up removing it since it's no longer needed in `CaptureFormControlRangePreEdit()`
if (RuntimeEnabledFeatures::FormControlRangeEnabled()) {
I think you always want to trace, not just when the feature is enabled.
Done
for (wtf_size_t i = 0; i < form_control_ranges_.size(); ++i) {
if (form_control_ranges_[i] == range) {
form_control_ranges_.EraseAt(i);
break;
}
}
Perhaps
```
auto iter = std::ranges::find(form_control_ranges_, range);
if (iter != form_control_ranges_.end()) {
form_control_ranges_.erase(iter);
}
```
Done
input_event.defaultPrevented() || form_control_ranges_.empty()) {
Does this ever get called if `defaultPrevented`? Seems like the only caller is in `DefaultEventHandler`, which shouldn't happen if the default was prevented. Perhaps you can just DCHECK?
Ended up removing `|input_event|` since it's only used for this check.
const unsigned max_by_sel = old_length - selection_end;
Hmm, `max_by_sel` is a little hard to parse. Maybe `maximum_suffix_length`?
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// Update |form_control_| after a text replacement at `change_offset`: removes
nit: I believe these should also be backticks. Same throughout the paragraph.
if (form_control_ != text_control) {
Should this have a comment like the blocks above?
DCHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
`CHECK` here instead? Since it's a `CHECK` above?
if (!RuntimeEnabledFeatures::FormControlRangeEnabled() ||
Should this also be:
```
CHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
if (form_control_ranges_.empty()) {
return;
}
```
? Or maybe `NotifyFormControlRangesOfTextChange` should just be in line since it appears to only be called once?
if (!RuntimeEnabledFeatures::FormControlRangeEnabled() ||
`CHECK` here instead?
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Code-Review | +1 |
Thanks for the changes! This looks pretty good to me.
// Pure insertion handling (no deletions), matching DOM Range behavior.
if (deleted_count == 0 && inserted_count > 0) {
// Collapsed insertion: caret remains before new text.
if (pre_start == pre_end && change_offset == pre_start) {
return;
}
// Insertion at range start: extend the end to include the new text.
if (pre_start != pre_end && change_offset == pre_start) {
end_offset_in_value_ = pre_end + inserted_count;
return;
}
// Insertion at range end: leave the range unchanged.
if (pre_start != pre_end && change_offset == pre_end) {
return;
}
}
Is this chunk just an optimization for this special case? (It seems like the logic below would handle this case correctly.) If so, at least let's add that to the comment.
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Commit-Queue | +1 |
// Update |form_control_| after a text replacement at `change_offset`: removes
nit: I believe these should also be backticks. Same throughout the paragraph.
Done
Should this have a comment like the blocks above?
Good idea - added.
DCHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
`CHECK` here instead? Since it's a `CHECK` above?
I’ve kept the `DCHECK` here to avoid unnecessary crashes. The constructor `CHECK` already ensures the feature is enabled at creation, so I think `DCHECK` should be sufficient.
// Pure insertion handling (no deletions), matching DOM Range behavior.
if (deleted_count == 0 && inserted_count > 0) {
// Collapsed insertion: caret remains before new text.
if (pre_start == pre_end && change_offset == pre_start) {
return;
}
// Insertion at range start: extend the end to include the new text.
if (pre_start != pre_end && change_offset == pre_start) {
end_offset_in_value_ = pre_end + inserted_count;
return;
}
// Insertion at range end: leave the range unchanged.
if (pre_start != pre_end && change_offset == pre_end) {
return;
}
}
Is this chunk just an optimization for this special case? (It seems like the logic below would handle this case correctly.) If so, at least let's add that to the comment.
Clarified in the comment: this is a special case to match DOM Range insertion behavior. The generic path mishandles caret/boundary cases. For example, a collapsed range at `[4,4]` with an insert at 4 should stay `[4,4]`, whereas the logic below moves it to `[7,7]`.
if (!RuntimeEnabledFeatures::FormControlRangeEnabled() ||
Should this also be:
```
CHECK(RuntimeEnabledFeatures::FormControlRangeEnabled());
if (form_control_ranges_.empty()) {
return;
}
```
? Or maybe `NotifyFormControlRangesOfTextChange` should just be in line since it appears to only be called once?
Added the DCHECK - thanks!
I've kept `NotifyFormControlRangesOfTextChange` for now just in case I need it later. But will definitely in-line it during later cleanup if it's not used anywhere else.
if (!RuntimeEnabledFeatures::FormControlRangeEnabled() ||
Stephanie Zhang`CHECK` here instead?
Changed to a DCHECK - thanks!
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Code-Review | +1 |
// Pure insertion handling (no deletions), matching DOM Range behavior.
if (deleted_count == 0 && inserted_count > 0) {
// Collapsed insertion: caret remains before new text.
if (pre_start == pre_end && change_offset == pre_start) {
return;
}
// Insertion at range start: extend the end to include the new text.
if (pre_start != pre_end && change_offset == pre_start) {
end_offset_in_value_ = pre_end + inserted_count;
return;
}
// Insertion at range end: leave the range unchanged.
if (pre_start != pre_end && change_offset == pre_end) {
return;
}
}
Stephanie ZhangIs this chunk just an optimization for this special case? (It seems like the logic below would handle this case correctly.) If so, at least let's add that to the comment.
Clarified in the comment: this is a special case to match DOM Range insertion behavior. The generic path mishandles caret/boundary cases. For example, a collapsed range at `[4,4]` with an insert at 4 should stay `[4,4]`, whereas the logic below moves it to `[7,7]`.
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Exportable changes to web-platform-tests were detected in this CL and a pull request in the upstream repo has been made: https://github.com/web-platform-tests/wpt/pull/55522.
When this CL lands, the bot will automatically merge the PR on GitHub if the required GitHub checks pass; otherwise, ecosystem-infra@ team will triage the failures and may contact you.
WPT Export docs:
https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md#Automatic-export-process
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
[FormControlRange] Live updates for user typing
Implement live offset updates for FormControlRange during user-initiated
edits (typing, backspace/delete, selection replacement). Like DOM Range,
updates are applied after the observable value mutation and before
'input' event listeners run. This ensures ranges reflect the new text
state within the same task in which the edit is observed.
Ranges shift, contract, or collapse to preserve coverage of the original
logical substring. A cancelled beforeinput leaves ranges unchanged. A
pre-edit snapshot taken at beforeinput is committed on the first
observable value change as a single replace span for stability.
Follow-up CL(s) for live updates will extend support to programmatic
edits and richer scenarios (e.g. drag/drop, IME).
Explainer: github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/FormControlRange/explainer.md
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
The WPT PR for this CL has been merged upstream! https://github.com/web-platform-tests/wpt/pull/55522
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |