diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesDisplayAttributePropertyRanges.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesDisplayAttributePropertyRanges.cs
index 94cf4ac7800..ff4a6158f17 100644
--- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesDisplayAttributePropertyRanges.cs
+++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesDisplayAttributePropertyRanges.cs
@@ -77,13 +77,6 @@ internal override void OnEndEdit(UnsafeNativeMethods.ITfContext context,
{
Guid displayAttributeGuid;
UnsafeNativeMethods.ITfProperty displayAttributeProperty;
- UnsafeNativeMethods.IEnumTfRanges attributeRangeEnumerator;
- UnsafeNativeMethods.ITfRange[] attributeRanges;
- int fetched;
- int guidAtom;
- TextServicesDisplayAttribute displayAttribute;
- ITextPointer start;
- ITextPointer end;
//
// Remove any existing display attribute highlights.
@@ -111,25 +104,166 @@ internal override void OnEndEdit(UnsafeNativeMethods.ITfContext context,
// Get the DisplayAttributeProperty.
displayAttributeGuid = Guid;
context.GetProperty(ref displayAttributeGuid, out displayAttributeProperty);
- // Get a range enumerator for the property.
- if (displayAttributeProperty.EnumRanges(ecReadOnly, out attributeRangeEnumerator, null) == NativeMethods.S_OK)
+
+ //
+ // Enumerate the display attribute property only where display attributes
+ // can currently be, instead of over the whole document.
+ //
+ // Passing null as the target range of EnumRanges enumerates the property
+ // across the entire document. The property keeps a range for every run the
+ // input method has composed, so on a long document this is a COM round trip
+ // per historical range, on every keystroke. Measured with the Windows 11
+ // Korean TSF IME on a 1,200 character document, a single OnEndEdit
+ // enumerated 1,077 ranges of which exactly one carried an attribute, and
+ // blocked the UI thread for roughly three seconds.
+ //
+ // Display attributes are decorations the input method places on text it is
+ // composing, so they can only be (a) inside an active composition or
+ // (b) on a range this edit just changed (which also covers ranges an ending
+ // composition just cleared). Enumerating the smallest span that covers both
+ // finds every range the whole-document scan would find - for any IME,
+ // including ones whose composition holds several attribute ranges at once,
+ // such as Japanese clause conversion, and edits that touch only some of
+ // them - while keeping the cost proportional to the composition rather than
+ // to the document.
+ //
+ if (TryGetAttributeScanWindow(context, ecReadOnly, editRecord, out int scanStart, out int scanEnd))
{
- attributeRanges = new UnsafeNativeMethods.ITfRange[1];
+ context.GetStart(ecReadOnly, out UnsafeNativeMethods.ITfRange scanRange);
+ ((UnsafeNativeMethods.ITfRangeACP)scanRange).SetExtent(scanStart, scanEnd - scanStart);
- // Walk each range.
- while (attributeRangeEnumerator.Next(1, attributeRanges, out fetched) == NativeMethods.S_OK)
- {
- // Get a DisplayAttribute for this range.
- guidAtom = GetInt32Value(ecReadOnly, displayAttributeProperty, attributeRanges[0]);
- displayAttribute = GetDisplayAttribute(guidAtom);
+ AddAttributeRanges(ecReadOnly, displayAttributeProperty, scanRange);
- if (displayAttribute != null && !displayAttribute.IsEmptyAttribute())
- {
- // Set a matching highlight for the attribute range.
- ConvertToTextPosition(attributeRanges[0], out start, out end);
+ Marshal.ReleaseComObject(scanRange);
+ }
+
+#if UNUSED_IME_HIGHLIGHT_LAYER
+ if (_highlightLayer != null)
+ {
+ this.TextStore.TextContainer.Highlights.AddLayer(_highlightLayer);
+ }
+#endif
- if (start != null)
+ if (_compositionAdorner != null)
+ {
+ // Update the layout to get the acurated rectangle from calling GetRectangleFromTextPosition
+ this.TextStore.RenderScope.UpdateLayout();
+
+ // Invalidate the composition adorner to render the composition attribute ranges.
+ _compositionAdorner.InvalidateAdorner();
+ }
+
+ Marshal.ReleaseComObject(displayAttributeProperty);
+ }
+
+ ///
+ /// Computes the smallest ACP span covering every active composition and
+ /// every range whose display attribute this edit changed. Returns false
+ /// when there is nothing to scan.
+ ///
+ private bool TryGetAttributeScanWindow(
+ UnsafeNativeMethods.ITfContext context,
+ int ecReadOnly,
+ UnsafeNativeMethods.ITfEditRecord editRecord,
+ out int scanStart,
+ out int scanEnd)
+ {
+ scanStart = int.MaxValue;
+ scanEnd = int.MinValue;
+ int fetched;
+
+ // (a) Active compositions.
+ if (context is UnsafeNativeMethods.ITfContextComposition contextComposition)
+ {
+ contextComposition.EnumCompositions(out UnsafeNativeMethods.IEnumITfCompositionView compositionEnumerator);
+ if (compositionEnumerator != null)
+ {
+ UnsafeNativeMethods.ITfCompositionView[] views = new UnsafeNativeMethods.ITfCompositionView[1];
+ while (compositionEnumerator.Next(1, views, out fetched) == NativeMethods.S_OK && fetched == 1)
+ {
+ views[0].GetRange(out UnsafeNativeMethods.ITfRange compositionRange);
+ if (compositionRange != null)
{
+ ExtendScanWindow(compositionRange, ref scanStart, ref scanEnd);
+ Marshal.ReleaseComObject(compositionRange);
+ }
+ Marshal.ReleaseComObject(views[0]);
+ }
+ Marshal.ReleaseComObject(compositionEnumerator);
+ }
+ }
+
+ // (b) Ranges whose display attribute changed in this edit.
+ UnsafeNativeMethods.IEnumTfRanges updatedRanges = GetPropertyUpdate(editRecord);
+ if (updatedRanges != null)
+ {
+ UnsafeNativeMethods.ITfRange[] updated = new UnsafeNativeMethods.ITfRange[1];
+ while (updatedRanges.Next(1, updated, out fetched) == NativeMethods.S_OK && fetched == 1)
+ {
+ ExtendScanWindow(updated[0], ref scanStart, ref scanEnd);
+ Marshal.ReleaseComObject(updated[0]);
+ }
+ Marshal.ReleaseComObject(updatedRanges);
+ }
+
+ return scanStart <= scanEnd;
+ }
+
+ private static void ExtendScanWindow(UnsafeNativeMethods.ITfRange range, ref int scanStart, ref int scanEnd)
+ {
+ ((UnsafeNativeMethods.ITfRangeACP)range).GetExtent(out int start, out int count);
+
+ // Cicero can report a negative length; ConvertToTextPosition guards the same way.
+ if (count < 0)
+ {
+ return;
+ }
+
+ if (start < scanStart)
+ {
+ scanStart = start;
+ }
+ if (start + count > scanEnd)
+ {
+ scanEnd = start + count;
+ }
+ }
+
+ ///
+ /// Adds every display attribute range found inside targetRange to the
+ /// composition adorner.
+ ///
+ private void AddAttributeRanges(
+ int ecReadOnly,
+ UnsafeNativeMethods.ITfProperty displayAttributeProperty,
+ UnsafeNativeMethods.ITfRange targetRange)
+ {
+ UnsafeNativeMethods.IEnumTfRanges attributeRangeEnumerator;
+
+ if (displayAttributeProperty.EnumRanges(ecReadOnly, out attributeRangeEnumerator, targetRange) != NativeMethods.S_OK)
+ {
+ return;
+ }
+
+ UnsafeNativeMethods.ITfRange[] attributeRanges = new UnsafeNativeMethods.ITfRange[1];
+ int fetched;
+
+ // Walk each range.
+ while (attributeRangeEnumerator.Next(1, attributeRanges, out fetched) == NativeMethods.S_OK)
+ {
+ // Get a DisplayAttribute for this range.
+ int guidAtom = GetInt32Value(ecReadOnly, displayAttributeProperty, attributeRanges[0]);
+ TextServicesDisplayAttribute displayAttribute = GetDisplayAttribute(guidAtom);
+
+ if (displayAttribute != null && !displayAttribute.IsEmptyAttribute())
+ {
+ // Set a matching highlight for the attribute range.
+ ITextPointer start;
+ ITextPointer end;
+ ConvertToTextPosition(attributeRanges[0], out start, out end);
+
+ if (start != null)
+ {
#if UNUSED_IME_HIGHLIGHT_LAYER
// Demand create the highlight layer.
if (_highlightLayer == null)
@@ -138,45 +272,26 @@ internal override void OnEndEdit(UnsafeNativeMethods.ITfContext context,
}
#endif
- if (_compositionAdorner == null)
- {
- _compositionAdorner = new CompositionAdorner(this.TextStore.TextView);
- _compositionAdorner.Initialize(this.TextStore.TextView);
- }
+ if (_compositionAdorner == null)
+ {
+ _compositionAdorner = new CompositionAdorner(this.TextStore.TextView);
+ _compositionAdorner.Initialize(this.TextStore.TextView);
+ }
#if UNUSED_IME_HIGHLIGHT_LAYER
// Need to pass the foreground and background color of the composition
_highlightLayer.Add(start, end, /*TextDecorationCollection:*/null);
#endif
- // Add the attribute range into CompositionAdorner.
- _compositionAdorner.AddAttributeRange(start, end, displayAttribute);
- }
+ // Add the attribute range into CompositionAdorner.
+ _compositionAdorner.AddAttributeRange(start, end, displayAttribute);
}
-
- Marshal.ReleaseComObject(attributeRanges[0]);
}
-#if UNUSED_IME_HIGHLIGHT_LAYER
- if (_highlightLayer != null)
- {
- this.TextStore.TextContainer.Highlights.AddLayer(_highlightLayer);
- }
-#endif
-
- if (_compositionAdorner != null)
- {
- // Update the layout to get the acurated rectangle from calling GetRectangleFromTextPosition
- this.TextStore.RenderScope.UpdateLayout();
-
- // Invalidate the composition adorner to render the composition attribute ranges.
- _compositionAdorner.InvalidateAdorner();
- }
-
- Marshal.ReleaseComObject(attributeRangeEnumerator);
+ Marshal.ReleaseComObject(attributeRanges[0]);
}
- Marshal.ReleaseComObject(displayAttributeProperty);
+ Marshal.ReleaseComObject(attributeRangeEnumerator);
}
// Callback from TextServicesProperty.OnLayoutUpdated.
diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesPropertyRanges.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesPropertyRanges.cs
index 521095be818..8f33439db63 100644
--- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesPropertyRanges.cs
+++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/TextServicesPropertyRanges.cs
@@ -147,8 +147,10 @@ protected static Object GetValue(int ecReadOnly, UnsafeNativeMethods.ITfProperty
///
/// Get ranges that the property is changed.
///
- private UnsafeNativeMethods.IEnumTfRanges GetPropertyUpdate(
- UnsafeNativeMethods.ITfEditRecord editRecord)
+ // Promoted from private to protected so TextServicesDisplayAttributePropertyRanges
+ // can reuse it to scope its enumeration to the ranges an edit actually changed.
+ protected UnsafeNativeMethods.IEnumTfRanges GetPropertyUpdate(
+ UnsafeNativeMethods.ITfEditRecord editRecord)
{
UnsafeNativeMethods.IEnumTfRanges ranges;