Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

/// <summary>
/// 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.
/// </summary>
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;
}
}

/// <summary>
/// Adds every display attribute range found inside targetRange to the
/// composition adorner.
/// </summary>
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)
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ protected static Object GetValue(int ecReadOnly, UnsafeNativeMethods.ITfProperty
/// <summary>
/// Get ranges that the property is changed.
/// </summary>
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;

Expand Down