Skip to content

extract reusable functions from plainTextView#27572

Open
daesunp wants to merge 8 commits into
microsoft:mainfrom
daesunp:refactor-plain-text-editor-view
Open

extract reusable functions from plainTextView#27572
daesunp wants to merge 8 commits into
microsoft:mainfrom
daesunp:refactor-plain-text-editor-view

Conversation

@daesunp

@daesunp daesunp commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Extracts reusable functions from plainTextView for shared use in our example app, and boards.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (652 lines, 6 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@daesunp daesunp marked this pull request as ready for review June 22, 2026 18:12
Copilot AI review requested due to automatic review settings June 22, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

Comment thread packages/framework/react/src/text/plain/usePlainTextInput.ts Outdated
Comment thread packages/framework/react/src/text/plain/useTreeSynchronizedString.ts Outdated
* @remarks
* Seeded from `initialSelection` and adjusted by {@link applyTextOps} as the text changes. It
* follows the same logical position across edits, but the hook does not observe the user's live
* caret, so a consumer that needs an accurate caret should track the element's selection itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow this remarks comment. Is this just saying that this should not be used to track cursor position?

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.

Updated remarks clarifying that if they want the real caret position should get it from the rendered element

Comment thread packages/framework/react/src/text/plain/useTreeSynchronizedString.ts Outdated
Comment thread packages/framework/react/src/text/plain/useTreeSynchronizedString.ts Outdated
Comment on lines +38 to +49
* The consumer supplies the other direction (string → tree) themselves, typically by calling
* {@link applyTextEdit} from their input's change handler:
*
* ```tsx
* const { text } = useTreeSynchronizedString(tree);
* return (
* <textarea
* value={text}
* onChange={(e) => applyTextEdit(tree, e.target.value)}
* />
* );
* ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I'm wondering if we may want to give a bit more prescriptive guidance here. For simple APIs like textarea, where the events only return the fully updated string, then recommending applyTextEdit probably makes sense. But I think we should probably recommend doing more properly incremental edits when possible.

We can probably get this across by noting that different text APIs offer different formats for change deltas, and so we don't offer a one-size-fits all solution for mapping those edits. But for simple APIs like textarea, we offer this naive implementation that can be used.

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.

Updated the remarks to note that different text apis report their edits in different formats

*/
export function useTreeSynchronizedString(
tree: TextAsTree.Tree,
initialSelection?: TextSelection,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably not for this PR, since it seems like we don't have a use-case for it yet. But I wonder if we should expand this to track 0 or more selections.

Comment thread packages/framework/react/src/text/plain/useTreeSynchronizedString.ts Outdated
Comment thread packages/framework/react/src/text/plain/useTreeSynchronizedString.ts Outdated
Comment thread packages/framework/react/src/text/plain/plainUtils.ts
Comment on lines +157 to +161
if (context.isBranch()) {
context.runTransaction(() => syncTextToTree(root, newText), { label });
} else {
syncTextToTree(root, newText);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this branching needed? context.runTransaction should be callable regardless of whether or not we're on a branch. If we're not, I believe the implementation does what your fallback case here does anyways.

And if this is the case, I'm not sure we need this applyTextEdit helper at all. Users can reasonably grab the node's context and call runTransaction and syncTextToTree on their own.

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.

removed if statement and updated to use context.runTransaction

@Josmithr Josmithr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few more comments. This one is likely the most critical: https://github.com/microsoft/FluidFramework/pull/27572/changes#r3516458250

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 3adb358594a36333b40463a2f8be3abcb07c782c
Head commit: bc15e83a013dcecef66aaf999da4b0e6309d6adf

Notable changes

  • 🔴 fluidFramework.js: parsed 392149 → 393022 (+873), gzip 111130 → 111633 (+503)
  • 🔴 sharedTree.js: parsed 381536 → 382402 (+866), gzip 108525 → 109016 (+491)
Per-bundle deltas

@fluid-example/bundle-size-tests

  • azureClient.js: parsed 619019 → 619154 (+135), gzip 164797 → 164872 (+75)
  • odspClient.js: parsed 591853 → 591880 (+27), gzip 158891 → 158958 (+67)
  • aqueduct.js: parsed 525463 → 525622 (+159), gzip 140683 → 140739 (+56)
  • 🔴 fluidFramework.js: parsed 392149 → 393022 (+873), gzip 111130 → 111633 (+503)
  • 🔴 sharedTree.js: parsed 381536 → 382402 (+866), gzip 108525 → 109016 (+491)
  • containerRuntime.js: parsed 303813 → 303951 (+138), gzip 83188 → 83222 (+34)
  • sharedString.js: parsed 175984 → 175991 (+7), gzip 49445 → 49453 (+8)
  • experimentalSharedTree.js: parsed 160798 → 160798 (0), gzip 45804 → 45804 (0)
  • matrix.js: parsed 159845 → 159852 (+7), gzip 45411 → 45418 (+7)
  • loader.js: parsed 145307 → 145270 (-37), gzip 39063 → 39076 (+13)
  • odspDriver.js: parsed 104431 → 104350 (-81), gzip 32647 → 32658 (+11)
  • directory.js: parsed 66616 → 66623 (+7), gzip 18532 → 18540 (+8)
  • 748.js: parsed 58793 → 58793 (0), gzip 17826 → 17826 (0)
  • map.js: parsed 46709 → 46716 (+7), gzip 14310 → 14318 (+8)
  • odspPrefetchSnapshot.js: parsed 45650 → 45656 (+6), gzip 15276 → 15284 (+8)
  • 594.js: parsed 44493 → 44493 (0), gzip 13744 → 13744 (0)
  • summarizerDelayLoadedModule.js: parsed 30753 → 30749 (-4), gzip 7767 → 7753 (-14)
  • socketModule.js: parsed 26486 → 26484 (-2), gzip 7883 → 7894 (+11)
  • createNewModule.js: parsed 12480 → 12480 (0), gzip 4786 → 4786 (0)
  • summaryModule.js: parsed 3797 → 3797 (0), gzip 1860 → 1860 (0)
  • connectionState.js: parsed 724 → 724 (0), gzip 429 → 429 (0)
  • sharedTreeAttributes.js: parsed 666 → 673 (+7), gzip 431 → 441 (+10)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 422 → 422 (0), gzip 316 → 316 (0)

* ```
* @internal
*/
export function applyTextEdit(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this helper function is needed. I think we can just let callers combine runTransaction and syncTextToTree as needed.

}
});

// The hook is a one-way tree → string sync; exercise it directly via renderHook.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These tests should live in the test module that corresponds with the source code module. For now, that would be useTreeSynchronizedString.spec.ts. That said, it might make sense to merge that function into plainUtils.ts, in which case these would move to plainUtilts.test.ts

Comment on lines +86 to +87
const clamp = (offset: number): number =>
Math.min(Math.max(offset, 0), reread.length);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We've defined clamp in 2 different places in this PR. My guess is that we have a clamp utility already somewhere that we should use. But if not, we should add this to arrayUtils (or someplace) so we can deduplicate.

Comment on lines +76 to +84
// No incremental delta is available for this change, so re-read the whole string.
// This happens when the character field's marks couldn't be composed into a single
// delta — e.g. the field was modified across multiple batches within one flush (such
// as an interleaved schema change) — or when the tree is out of sync with the delta.
// Any tracked selection is clamped into the new text, since a shrinking edit could
// otherwise leave it out of bounds.
const reread = tree.fullString();
textRef.current = reread;
setText(reread);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this correct for all cases? I can imagine a case where a batched set of changes include deleting all characters, then inserting other characters. In a normal case, this would (presumably) result in the selection being effectively removed. But in this case, it would result in unrelated characters being selected.

I haven't thought through all of the relevant cases here, but I have a suspicion that this isn't the policy we want here.

I would recommend thinking through the cases here and ensuring we have them covered by tests. Since this is a helper utility, rather than built-in functionality, it's probably okay for there to be edge cases we don't handle well. But we should make sure these cases and associated policy are clearly documented, and that the policy we go with is the one that will cause least disruption.

});

// The hook is a one-way tree → string sync; exercise it directly via renderHook.
describe("useTreeSynchronizedString", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suspect we should probably have many more test cases than this. The code has a number of branches and edge cases we should make sure we are covering. E.g. cases where the selection gets clamped, cases where the selection is reduced to 0 characters, etc.

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