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;