Skip to content

Fixed selection attributes handling after softBreak.#19918

Open
arkflpc wants to merge 8 commits intomasterfrom
ck/19853-softBreakAndSelection
Open

Fixed selection attributes handling after softBreak.#19918
arkflpc wants to merge 8 commits intomasterfrom
ck/19853-softBreakAndSelection

Conversation

@arkflpc
Copy link
Copy Markdown
Contributor

@arkflpc arkflpc commented Mar 6, 2026

🚀 Summary

Improved document selection attribute inheritance around <softBreak> so returning the caret after a soft break restores expected formatting.

When selection attributes are recalculated across <softBreak>, only attributes marked with copyOnEnter are inherited. Other inline non-object elements still act as hard boundaries.


📌 Related issues


💡 Additional information

Optional: Notes on decisions, edge cases, or anything helpful for reviewers.


🧾 Checklists

Use the following checklists to ensure important areas were not overlooked.
This does not apply to feature-branch merges.
If an item is not relevant to this type of change, simply leave it unchecked.

Author checklist

  • Is the changelog entry intentionally omitted?
  • Is the change backward-compatible?
  • Have you considered the impact on different editor setups and core interactions? (e.g., classic/inline/multi-root/many editors, typing, selection, paste, tables, lists, images, collaboration, pagination)
  • Has the change been manually verified in the relevant setups?
  • Does this change affect any of the above?
  • Is performance impacted?
  • Is accessibility affected?
  • Have tests been added that fail without this change (against regression)?
  • Have the API documentation, guides, feature digest, and related feature sections been updated where needed?
  • Have metadata files (ckeditor5-metadata.json) been updated if needed?
  • Are there any changes the team should be informed about (e.g. architectural, difficult to revert in future versions or having impact on other features)?
  • Were these changes documented (in Logbook)?

Reviewer checklist

  • PR description explains the changes and the chosen approach (especially when performance, API, or UX is affected).
  • The changelog entry is clear, user‑ or integrator-facing, and it describes any breaking changes.
  • All new external dependencies have been approved and mentioned in LICENSE.md (if any).
  • All human-readable, translateable strings in this PR been introduced using t() (if any).
  • I manually verified the change (e.g., in manual tests or documentation).
  • The target branch is correct.

@arkflpc arkflpc requested review from Mati365 and niegowski March 6, 2026 11:25
Copy link
Copy Markdown
Member

@Mati365 Mati365 left a comment

Choose a reason for hiding this comment

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

Looks ok. I left one comment. Make sure manual QA testing is performed, and E2E testing was executed on this PR.

@Mati365
Copy link
Copy Markdown
Member

Mati365 commented Mar 9, 2026

@arkflpc Please merge recent master, or cherry-pick 1d846d2 in order to fix failing manual tests.

Comment on lines +1260 to +1264
for ( let node: ModelNode | null = startNode; node; node = getNextNode( node ) ) {
if ( node.is( 'element', 'softBreak' ) ) {
crossedSoftBreak = true;
continue;
}
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.

I'm not sure whether the engine should be aware of the existence of the softBreak element. It's registered in an external plugin. Would a schema flag be better? What do you think?

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.

We discussed with @niegowski, and it might be good idea to store some info in schema / attribute properties. However, there might be another workaround related to storing attributes on soft-breaks itself. We need to discuss it a little bit more.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Engine core hardcodes plugin-specific element name
    • Replaced the hardcoded softBreak element check with schema-based inline non-object handling that copies only $text attributes marked copyOnEnter, removing plugin-name coupling in engine selection logic.

Create PR

Or push these changes by commenting:

@cursor push 09cbb82ebf
Preview (09cbb82ebf)
diff --git a/packages/ckeditor5-engine/src/model/documentselection.ts b/packages/ckeditor5-engine/src/model/documentselection.ts
--- a/packages/ckeditor5-engine/src/model/documentselection.ts
+++ b/packages/ckeditor5-engine/src/model/documentselection.ts
@@ -1278,11 +1278,20 @@
 			return node.getAttributes();
 		}
 
-		if ( node.is( 'element', 'softBreak' ) ) {
+		if ( !schema.isInline( node ) ) {
+			return null;
+		}
+
+		if ( !schema.isObject( node ) ) {
 			const attributes: Array<[ string, unknown ]> = [];
 
+			// Inline non-object elements create a "soft" attribute boundary and can optionally carry over
+			// text attributes marked as copyOnEnter.
 			for ( const [ key, value ] of node.getAttributes() ) {
-				if ( schema.checkAttribute( '$text', key ) ) {
+				if (
+					schema.checkAttribute( '$text', key ) &&
+					schema.getAttributeProperties( key ).copyOnEnter
+				) {
 					attributes.push( [ key, value ] );
 				}
 			}
@@ -1290,14 +1299,6 @@
 			return attributes;
 		}
 
-		if ( !schema.isInline( node ) ) {
-			return null;
-		}
-
-		if ( !schema.isObject( node ) ) {
-			return [];
-		}
-
 		const attributes: Array<[string, unknown]> = [];
 
 		// Collect all attributes that can be applied to the text node.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

}

if ( !schema.isInline( node ) ) {
continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-inline elements skipped instead of halting backward/forward scan

Low Severity

When getTextAttributes encounters a non-inline element (a block element like <paragraph>) during a backward or forward scan, it uses continue to skip it. While the old code's getTextAttributes also returned null for non-inline elements (causing the old while-loop to similarly continue past them), the old loop additionally checked whether the starting node itself was truthy (while ( node && !attrs )), meaning it could only traverse real siblings. The new siblingNodes generator also handles null correctly, so in practice this produces the same result. However, the JSDoc comment at line 1237 now says "it checks if item is an inline object" but the function actually processes all inline elements (both object and non-object), not just inline objects. This could mislead future contributors about the function's scope.

Fix in Cursor Fix in Web

writer.removeAttribute( key, child );
wasChanged = true;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Iterating attributes while removing them from Map

Low Severity

child.getAttributes() returns a live Map.entries() iterator, and writer.removeAttribute(key, child) deletes from that same Map during iteration. While deleting the current entry is generally safe in ES6 Map iteration, the writer.removeAttribute path goes through model.applyOperation, which fires events and could theoretically trigger listeners that add or remove other attributes on the same child, potentially skipping or double-visiting entries. Collecting keys to remove first (like into an array) and then removing them after the loop would be more robust.

Fix in Cursor Fix in Web

@Mati365 Mati365 self-requested a review April 7, 2026 08:40
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.

Double Shift+Enter should preserve selection attributes.

3 participants