Code:
/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / wpf / src / Framework / System / Windows / Documents / Speller.cs / 1 / Speller.cs
//----------------------------------------------------------------------------
//
// File: Speller.cs
//
// Description: Spell checking component for the TextEditor.
//
//---------------------------------------------------------------------------
namespace System.Windows.Documents
{
using MS.Internal;
using System.Threading;
using System.Windows.Threading;
using System.Globalization;
using System.Collections;
using System.Security;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using MS.Win32;
using MS.Internal.PresentationFramework;
using System.Windows.Controls;
using System.Windows.Markup; // XmlLanguage
using System.Windows.Input;
// Spell checking component for the TextEditor.
internal class Speller
{
//-----------------------------------------------------
//
// Constructors
//
//-----------------------------------------------------
#region Constructors
// Creates a new instance. We have at most one Speller instance
// per TextEditor.
internal Speller(TextEditor textEditor)
{
_textEditor = textEditor;
_textEditor.TextContainer.Change += new TextContainerChangeEventHandler(OnTextContainerChange);
// Schedule some idle time to start examining the document.
if (_textEditor.TextContainer.SymbolCount > 0)
{
ScheduleIdleCallback();
}
_defaultCulture = InputLanguageManager.Current != null ? InputLanguageManager.Current.CurrentInputLanguage :
Thread.CurrentThread.CurrentCulture;
}
#endregion Constructors
//------------------------------------------------------
//
// Internal Methods
//
//-----------------------------------------------------
#region Internal Methods
// Called by TextEditor to disable spelling.
///
/// Critical - this code resets the _textChunk member which is critical
/// TreatAsSafe - the operation is safe to expose
///
[SecurityCritical, SecurityTreatAsSafe]
internal void Detach()
{
Invariant.Assert(_textEditor != null);
_textEditor.TextContainer.Change -= new TextContainerChangeEventHandler(OnTextContainerChange);
if (_pendingCaretMovedCallback)
{
_textEditor.Selection.Changed -= new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus -= new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = false;
}
// Shutdown the highlight layer.
if (_highlightLayer != null)
{
_textEditor.TextContainer.Highlights.RemoveLayer(_highlightLayer);
_highlightLayer = null;
}
// Shutdown the status table.
_statusTable = null;
// Release our nl6 objects.
if (_spellerInterop != null)
{
_spellerInterop.Dispose();
_spellerInterop = null;
}
// Clear the TextEditor. (Used as a sentinel to track Detachedness
// from pending idle callback.)
_textEditor = null;
}
// Returns an object holding state about an error at the specified
// position, or null if no error is present.
//
// If forceEvaluation is set true, the speller will analyze any dirty region
// covered by the position. Otherwise dirty regions will be treated as
// non-errors.
internal SpellingError GetError(ITextPointer position, LogicalDirection direction, bool forceEvaluation)
{
ITextPointer start;
ITextPointer end;
SpellingError error;
// Evaluate any pending dirty region.
if (forceEvaluation &&
EnsureInitialized() &&
_statusTable.IsRunType(position.CreateStaticPointer(), direction, SpellerStatusTable.RunType.Dirty))
{
ScanPosition(position, direction);
}
// Get the error result.
if (_statusTable != null &&
_statusTable.GetError(position.CreateStaticPointer(), direction, out start, out end))
{
error = new SpellingError(this, start, end);
}
else
{
error = null;
}
return error;
}
// Worker for TextBox/RichTextBox.GetNextSpellingErrorPosition.
// Returns the start position of the next error, or null if no error exists.
//
// NB: this method will force an evaluation of any dirty regions between
// position and the next error, which in the worst case is the rest of
// the document.
internal ITextPointer GetNextSpellingErrorPosition(ITextPointer position, LogicalDirection direction)
{
if (!EnsureInitialized())
return null;
StaticTextPointer scanPosition = position.CreateStaticPointer();
StaticTextPointer endPosition;
SpellerStatusTable.RunType runType;
while (_statusTable.GetRun(scanPosition, direction, out runType, out endPosition))
{
if (runType == SpellerStatusTable.RunType.Error)
break;
if (runType == SpellerStatusTable.RunType.Dirty)
{
ScanPosition(scanPosition.CreateDynamicTextPointer(direction), direction);
_statusTable.GetRun(scanPosition, direction, out runType, out endPosition);
Invariant.Assert(runType != SpellerStatusTable.RunType.Dirty);
if (runType == SpellerStatusTable.RunType.Error)
break;
}
scanPosition = endPosition;
}
SpellingError spellingError = GetError(scanPosition.CreateDynamicTextPointer(direction), direction, false /* forceEvaluation */);
return spellingError == null ? null : spellingError.Start;
}
// Called by SpellingError to retreive a list of suggestions
// for an error range.
// This method actually runs the speller on the specified text,
// re-evaluating the error from scratch.
///
/// Critical - It calls SetContextOption() which is Critical.
/// TreatAsSafe - it calls them with trusted parameters.
///
[SecurityCritical, SecurityTreatAsSafe]
internal IList GetSuggestionsForError(SpellingError error)
{
ITextPointer contextStart;
ITextPointer contextEnd;
ITextPointer contentStart;
ITextPointer contentEnd;
TextMap textMap;
ArrayList suggestions;
suggestions = new ArrayList(1);
//
// IMPORTANT!!
//
// This logic here must match ScanRange, or else we might not
// calculate the exact same error. Keep the two methods in [....]!
//
XmlLanguage language;
CultureInfo culture = GetCurrentCultureAndLanguage(error.Start, out language);
if (culture == null || !CanSpellCheck(culture))
{
// Return an empty list.
}
else
{
ExpandToWordBreakAndContext(error.Start, LogicalDirection.Backward, language, out contentStart, out contextStart);
ExpandToWordBreakAndContext(error.End, LogicalDirection.Forward, language, out contentEnd, out contextEnd);
textMap = new TextMap(contextStart, contextEnd, contentStart, contentEnd);
SetCulture(culture);
_spellerInterop.SetContextOption("IsSpellChecking", true);
_spellerInterop.SetContextOption("IsSpellVerifyOnly", false);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ScanErrorTextSegment), new TextMapCallbackData(textMap, suggestions));
}
return suggestions;
}
// Worker for context menu's "Ignore All" item.
// Adds a word to the ignore list, and clears any matching errors.
//
//
internal void IgnoreAll(string word)
{
if (_ignoredWordsList == null)
{
_ignoredWordsList = new ArrayList(1);
}
int index = _ignoredWordsList.BinarySearch(word, new CaseInsensitiveComparer(_defaultCulture));
if (index < 0)
{
// This is a new word to ignore.
// Add it the list so we don't flag it later.
_ignoredWordsList.Insert(~index, word);
// Then search through the error list, clearing any matching
// errors.
if (_statusTable != null)
{
StaticTextPointer pointer = _textEditor.TextContainer.CreateStaticPointerAtOffset(0);
ITextPointer errorStart;
ITextPointer errorEnd;
Char[] charArray = null;
while (!pointer.IsNull)
{
if (_statusTable.GetError(pointer, LogicalDirection.Forward, out errorStart, out errorEnd))
{
string error = TextRangeBase.GetTextInternal(errorStart, errorEnd, ref charArray);
if (String.Compare(word, error, true /* ignoreCase */, _defaultCulture) == 0)
{
_statusTable.MarkCleanRange(errorStart, errorEnd);
}
}
pointer = _statusTable.GetNextErrorTransition(pointer, LogicalDirection.Forward);
}
}
}
}
// Sets the speller engine spelling reform option.
internal void SetSpellingReform(SpellingReform spellingReform)
{
if (_spellingReform != spellingReform)
{
_spellingReform = spellingReform;
// Invalidate the whole document.
ResetErrors();
}
}
// Called when a global state change invalidates all cached errors.
internal void ResetErrors()
{
if (_statusTable != null)
{
_statusTable.MarkDirtyRange(_textEditor.TextContainer.Start, _textEditor.TextContainer.End);
if (_textEditor.TextContainer.SymbolCount > 0)
{
ScheduleIdleCallback();
}
}
}
// Returns true if the specified property affects speller evaluation.
internal static bool IsSpellerAffectingProperty(DependencyProperty property)
{
return property == FrameworkElement.LanguageProperty ||
property == SpellCheck.SpellingReformProperty;
}
#endregion Internal methods
//------------------------------------------------------
//
// Internal Properties
//
//------------------------------------------------------
#region Internal Properties
// A run-length array tracking speller status of all text in the document.
internal SpellerStatusTable StatusTable
{
get
{
return _statusTable;
}
}
#endregion Internal Properties
//-----------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// Initializes state for the Speller.
// Delayed until the first text change event, or first idle callback.
///
/// Critical - This code calls into _textchunk and other unmanaged COM api.
/// TreatAsSafe - critical operations are not based on untrusted input. multiple calls don't involve any risk.
///
[SecurityCritical, SecurityTreatAsSafe]
private bool EnsureInitialized()
{
if (_spellerInterop != null)
return true;
if (_failedToInit)
return false;
Invariant.Assert(_highlightLayer == null);
Invariant.Assert(_statusTable == null);
try
{
_spellerInterop = new SpellerInterop();
}
catch (DllNotFoundException)
{
_failedToInit = true;
}
catch (EntryPointNotFoundException)
{
_failedToInit = true;
}
if (_failedToInit)
return false;
_highlightLayer = new SpellerHighlightLayer(this);
_statusTable = new SpellerStatusTable(_textEditor.TextContainer.Start, _highlightLayer);
_textEditor.TextContainer.Highlights.AddLayer(_highlightLayer);
//
_spellerInterop.SetContextOption("IsSpellSuggestingMWEs", false);
_spellingReform = (SpellingReform)_textEditor.UiScope.GetValue(SpellCheck.SpellingReformProperty);
return true;
}
// Posts a background priority operation to the dispatcher queue.
// All scanning takes place during idle-time callbacks.
private void ScheduleIdleCallback()
{
if (!_pendingIdleCallback)
{
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new DispatcherOperationCallback(OnIdle), null);
_pendingIdleCallback = true;
}
}
// Enables the TextSelection.Changed listener.
// We call this method when an otherwise clean document has text
// covered by the caret or an IME composition that must be analyzed
// when the selection moves away.
private void ScheduleCaretMovedCallback()
{
if (!_pendingCaretMovedCallback)
{
_textEditor.Selection.Changed += new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus += new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = true;
}
}
// Callback for document changes.
// Marks appropriate sections of the document as dirty then posts
// an idle request for future analysis.
private void OnTextContainerChange(object sender, TextContainerChangeEventArgs e)
{
Invariant.Assert(sender == _textEditor.TextContainer);
if (e.Count == 0 ||
(e.TextChange == TextChangeType.PropertyModified && !IsSpellerAffectingProperty(e.Property)))
{
// Speller doesn't care about most property changes.
return;
}
if (_failedToInit)
{
// Speller engine is not available.
return;
}
if (_statusTable != null)
{
_statusTable.OnTextChange(e);
}
ScheduleIdleCallback();
}
// Runs the speller idle callback.
// During this callback, we scan dirty portions of the document
// until all text is examined, or we exceed a set time limit.
// If we run out of time with more work to do, we post a new idle
// callback request, yielding to any pending high-priority work
// (such as user input).
//
private object OnIdle(object unused)
{
Invariant.Assert(_pendingIdleCallback);
// Reset _pendingIdleCallback.
_pendingIdleCallback = false;
// _textEditor will be null if we've been detached since requesting the callback.
if (_textEditor != null &&
EnsureInitialized())
{
ITextPointer start;
ITextPointer end;
long timeLimit;
ScanStatus status;
timeLimit = DateTime.Now.Ticks + MaxIdleTimeSliceNs;
end = null;
status = null;
// Iterate over chunks of dirty text until we run out of time
// or finish with the entire document.
do
{
if (!GetNextScanRange(end, out start, out end))
break;
status = ScanRange(start, end, timeLimit);
}
while (!status.HasExceededTimeLimit);
// Schedule any pending work before we yield.
if (status != null)
{
if (status.HasExceededTimeLimit)
{
ScheduleIdleCallback();
}
}
}
return null;
}
// Callback for TextSelection.Changed event.
private void OnCaretMoved(object sender, EventArgs e)
{
OnCaretMovedWorker();
}
// Callback for UiScope.LostFocus event.
private void OnLostFocus(object sender, RoutedEventArgs e)
{
OnCaretMovedWorker();
}
// Callback for the TextSelection.Changed or UiScope.LostFocus events.
// We enter this method when an otherwise clean document has text
// covered by the caret or an IME composition that must be analyzed
// when the selection moves away.
private void OnCaretMovedWorker()
{
if (!_pendingCaretMovedCallback || _textEditor == null)
{
// Because the event route caches the callback, we can get a
// callback even after removing the handler.
// Just ignore the spurious callback.
return;
}
_textEditor.Selection.Changed -= new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus -= new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = false;
// Now that the caret's out of the way, analyze the text it
// used to cover the next time the app goes idle.
ScheduleIdleCallback();
}
// Calculates the next run of text to feed to the speller.
// If there's nothing left to analyze, returns false and start/end will
// be null on exit.
private bool GetNextScanRange(ITextPointer searchStart, out ITextPointer start, out ITextPointer end)
{
ITextPointer rawStart;
ITextPointer rawEnd;
start = null;
end = null;
//
// First iteration of the scan loop, searchStart == null.
if (searchStart == null)
{
searchStart = _textEditor.TextContainer.Start;
}
// Grab the first dirty range.
GetNextScanRangeRaw(searchStart, out rawStart, out rawEnd);
if (rawStart != null)
{
// Skip over the caret and/or IME composition.
AdjustScanRangeAroundComposition(rawStart, rawEnd, out start, out end);
}
return start != null;
}
// Finds the next dirty range following searchStart, without considering
// the current caret or IME composition.
private void GetNextScanRangeRaw(ITextPointer searchStart, out ITextPointer start, out ITextPointer end)
{
Invariant.Assert(searchStart != null);
start = null;
end = null;
// Grab the first dirty range.
_statusTable.GetFirstDirtyRange(searchStart, out start, out end);
if (start != null)
{
Invariant.Assert(start.CompareTo(end) < 0);
// Cap the block size by a constant.
if (start.GetOffsetToPosition(end) > MaxScanBlockSize)
{
end = start.CreatePointer(MaxScanBlockSize);
}
// Ensure the block has constant language.
XmlLanguage language = (XmlLanguage)start.GetValue(FrameworkElement.LanguageProperty);
end = GetNextLanguageTransition(start, LogicalDirection.Forward, language, end);
Invariant.Assert(start.CompareTo(end) < 0);
}
}
// Truncates a range of text if it overlaps the word containing the
// caret, or an IME composition.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private void AdjustScanRangeAroundComposition(ITextPointer rawStart, ITextPointer rawEnd,
out ITextPointer start, out ITextPointer end)
{
start = rawStart;
end = rawEnd;
if (!_textEditor.Selection.IsEmpty)
{
// No caret to adjust around.
return;
}
if (!_textEditor.UiScope.IsKeyboardFocused)
{
// Document isn't focused, no caret rendered.
return;
}
// Get the word surrounding the caret.
ITextPointer wordBreakLeft;
ITextPointer wordBreakRight;
ITextPointer caretPosition;
TextMap textMap;
ArrayList segments;
caretPosition = _textEditor.Selection.Start;
// Disable spell checking functionality since we're only
// interested in word breaks here. This greatly cuts down
// the engine's workload.
_spellerInterop.SetContextOption("IsSpellChecking", false);
XmlLanguage language = (XmlLanguage)caretPosition.GetValue(FrameworkElement.LanguageProperty);
wordBreakLeft = SearchForWordBreaks(caretPosition, LogicalDirection.Backward, language, 1, false /* stopOnError */);
wordBreakRight = SearchForWordBreaks(caretPosition, LogicalDirection.Forward, language, 1, false /* stopOnError */);
textMap = new TextMap(wordBreakLeft, wordBreakRight, caretPosition, caretPosition);
segments = new ArrayList(2);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ExpandToWordBreakCallback), segments);
// We will have no segments when position is surrounded by
// nothing but white space.
if (segments.Count != 0)
{
int leftBreakOffset;
int rightBreakOffset;
// Figure out where caretPosition lives in the segment list.
FindPositionInSegmentList(textMap, LogicalDirection.Backward, segments, out leftBreakOffset, out rightBreakOffset);
wordBreakLeft = textMap.MapOffsetToPosition(leftBreakOffset);
wordBreakRight = textMap.MapOffsetToPosition(rightBreakOffset);
}
// Overlap?
if (wordBreakLeft.CompareTo(rawEnd) < 0 &&
wordBreakRight.CompareTo(rawStart) > 0)
{
if (wordBreakLeft.CompareTo(rawStart) > 0)
{
// Truncate the right half of the input range.
end = wordBreakLeft;
}
else if (wordBreakRight.CompareTo(rawEnd) < 0)
{
// Truncate the left half of the input range.
start = wordBreakRight;
}
else
{
// The entire dirty range is covered by the caret word.
// Try to find a following dirty range.
GetNextScanRangeRaw(wordBreakRight, out start, out end);
}
// Schedule a future callback to deal with the skipped
// overlapping section.
ScheduleCaretMovedCallback();
}
}
// Analyzes a run of text. The scan may be interrupted if we run out
// of time along the way, in which case some subset of the contained
// words will be left dirty.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private ScanStatus ScanRange(ITextPointer start, ITextPointer end, long timeLimit)
{
ITextPointer contextStart;
ITextPointer contextEnd;
ITextPointer contentStart;
ITextPointer contentEnd;
TextMap textMap;
ScanStatus status;
//
// IMPORTANT: the scan logic here (word break expansion, TextMap creation, etc.)
// must match GetSuggestionForError exactly. Keep the methods in [....]!
//
//
// Expand the content to include whole words.
// Also get pointers to sufficient surrounding text to analyze
// multi-word errors correctly.
//
status = new ScanStatus(timeLimit);
XmlLanguage language;
CultureInfo culture = GetCurrentCultureAndLanguage(start, out language);
if (culture == null)
{
// Someone set a bogus language on the run -- ignore it.
_statusTable.MarkCleanRange(start, end);
}
else
{
SetCulture(culture);
ExpandToWordBreakAndContext(start, LogicalDirection.Backward, language, out contentStart, out contextStart);
ExpandToWordBreakAndContext(end, LogicalDirection.Forward, language, out contentEnd, out contextEnd);
Invariant.Assert(contentStart.CompareTo(contentEnd) < 0);
Invariant.Assert(contextStart.CompareTo(contextEnd) < 0);
Invariant.Assert(contentStart.CompareTo(contextStart) >= 0);
Invariant.Assert(contentEnd.CompareTo(contextEnd) <= 0);
//
// Mark the range clean, before we scan for errors.
//
_statusTable.MarkCleanRange(contentStart, contentEnd);
//
// Read the text.
//
// Check for a compatible language.
if (CanSpellCheck(culture))
{
_spellerInterop.SetContextOption("IsSpellChecking", true);
_spellerInterop.SetContextOption("IsSpellVerifyOnly", true);
textMap = new TextMap(contextStart, contextEnd, contentStart, contentEnd);
//
// Iterate over sentences and segments.
//
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, new SpellerInterop.EnumSentencesCallback(ScanRangeCheckTimeLimitCallback),
new SpellerInterop.EnumTextSegmentsCallback(ScanTextSegment), new TextMapCallbackData(textMap, status));
if (status.TimeoutPosition != null)
{
if (status.TimeoutPosition.CompareTo(end) < 0)
{
// We ran out of time before analyzing the whole block.
// Reset the dirty status of the remainder.
_statusTable.MarkDirtyRange(status.TimeoutPosition, end);
// We should always make some forward progress, even just one word,
// otherwise we'll never finish checking the document.
if (status.TimeoutPosition.CompareTo(start) <= 0)
{
// Diagnostic info for bug 1577085.
string debugMessage = "Speller is not advancing! \n" +
"Culture = " + culture + "\n" +
"Start offset = " + start.Offset + " parent = " + start.ParentType.Name + "\n" +
"ContextStart offset = " + contextStart.Offset + " parent = " + contextStart.ParentType.Name + "\n" +
"ContentStart offset = " + contentStart.Offset + " parent = " + contentStart.ParentType.Name + "\n" +
"ContentEnd offset = " + contentEnd.Offset + " parent = " + contentEnd.ParentType.Name + "\n" +
"ContextEnd offset = " + contextEnd.Offset + " parent = " + contextEnd.ParentType.Name + "\n" +
"Timeout offset = " + status.TimeoutPosition.Offset + " parent = " + status.TimeoutPosition.ParentType.Name + "\n" +
"textMap TextLength = " + textMap.TextLength + " text = " + new string(textMap.Text) + "\n" +
"Document = " + start.TextContainer.Parent.GetType().Name + "\n";
if (start is TextPointer)
{
debugMessage += "Xml = " + new TextRange((TextPointer)start.TextContainer.Start, (TextPointer)start.TextContainer.End).Xml;
}
Invariant.Assert(false, debugMessage);
}
}
else
{
// We ran of time but finished the whole block.
// TimeoutPosition should never be past contentEnd.
// It might be less than contentEnd if the dirty run ends
// with an element edge, in which case TimeoutPosition
// will preceed the final element edge(s).
Invariant.Assert(status.TimeoutPosition.CompareTo(contentEnd) <= 0);
}
}
}
}
return status;
}
// Callback for the error segment scanned during error lookup.
// Returns a list of correction suggestions.
private bool ScanErrorTextSegment(object textSegment, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
SpellerInterop.STextRange sTextRange = SpellerInterop.GetSegmentRange(textSegment);
// Check if this segment falls outside the content range.
// The region before/after the content is only for context --
// to handle multi-word errors correctly. We never want to mark it.
if (sTextRange.Start + sTextRange.Length <= data.TextMap.ContentStartOffset)
{
// Preceeding context, skip this segment and keep going.
return true;
}
SpellerInterop.GetSuggestions(textSegment, (ArrayList)data.Data);
// We only expect one error segment for this callback, so skip any
// following context segments.
return false;
}
// Scans a single segment (the engine's formal notion of a "word").
// Called indirectly by ScanRange.
// Returns true to continue the segment enumeration, false to
// break out of the iteration.
private bool ScanTextSegment(object textSegment, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
SpellerInterop.STextRange sTextRange;
char[] word;
sTextRange = SpellerInterop.GetSegmentRange(textSegment);
// Check if this segment falls outside the content range.
// The region before/after the content is only for context --
// to handle multi-word errors correctly. We never want to mark it.
if (sTextRange.Start + sTextRange.Length <= data.TextMap.ContentStartOffset)
{
// Preceeding context, skip this segment and keep going.
return true;
}
if (sTextRange.Start >= data.TextMap.ContentEndOffset)
{
// Following context, skip this segment and stop iterating any remainder.
return false;
}
if (sTextRange.Length > 1) // Ignore single letter errors.
{
// Check if the segment has been marked "ignore" by the user.
word = new char[sTextRange.Length];
Array.Copy(data.TextMap.Text, sTextRange.Start, word, 0, sTextRange.Length);
if (!IsIgnoredWord(word))
{
SpellerInterop.RangeRole role = SpellerInterop.GetSegmentRole(textSegment);
if (role == SpellerInterop.RangeRole.ecrrIncorrect)
{
if (SpellerInterop.GetSegmentCount(textSegment) == 0)
{
// We have an error.
MarkErrorRange(data.TextMap, sTextRange);
}
else
{
// We have a subsegment with an error.
SpellerInterop.EnumSubsegments(textSegment,
new SpellerInterop.EnumTextSegmentsCallback(ScanTextSegment), data);
}
}
}
}
return true;
}
// Called after we've scanned all the segments within a sentence from inside ScanRange.
// This method returns false to the enumerator to end the scan if we've run out of time.
//
// NB: we terminate the segment enumeration run by ScanRange only at sentence boundaries,
// not at more finely grained segment boundaries. This is because the vast amount of
// work done takes place when preparing to iterate segments, so the incremental cost
// of actually walking the segments once calculated is ignorable, but halting the scan
// after looking at a single segment would mean repeating the overhead on all segments
// in the sentence.
private bool ScanRangeCheckTimeLimitCallback(object sentence, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
ScanStatus status;
status = (ScanStatus)data.Data;
// Stop iterating if we exceed our time budget.
// In which case, take note of where we left off.
if (status.HasExceededTimeLimit)
{
Invariant.Assert(status.TimeoutPosition == null); // We should only set this once....
int sentenceEndOffset = SpellerInterop.GetSentenceEndOffset(sentence);
if (sentenceEndOffset >= 0)
{
// The end of this segment may extend past textMap.ContentEndOffset,
// in the case of multi-word errors. So truncate.
//
int timeOutOffset = Math.Min(data.TextMap.ContentEndOffset, sentenceEndOffset);
// Be careful not to stop the iteration if we haven't reached
// the content start yet. It's possible that the context text
// will extend backwards into another sentence. We must always
// make forward progress, even if doing so exceeds the timeout
// limit, otherwise we'll get stuck infinitely eating idle time.
//
//
if (timeOutOffset > data.TextMap.ContentStartOffset)
{
status.TimeoutPosition = data.TextMap.MapOffsetToPosition(timeOutOffset);
}
}
}
return (status.TimeoutPosition == null);
}
// Flags a run of text with an error.
// In two exceptional circumstances we schedule an idle-time callback
// to re-analyze the run instead of marking it:
// - when the caret is within the error text.
// - when an IME composition covers the text.
private void MarkErrorRange(TextMap textMap, SpellerInterop.STextRange sTextRange)
{
ITextPointer errorStart;
ITextPointer errorEnd;
if (sTextRange.Start + sTextRange.Length > textMap.ContentEndOffset)
{
// We found an error that starts in the content but extends into
// the context. This must be a multi-word error.
// For now, ignore it.
//
return;
}
errorStart = textMap.MapOffsetToPosition(sTextRange.Start);
errorEnd = textMap.MapOffsetToPosition(sTextRange.Start + sTextRange.Length);
if (sTextRange.Start < textMap.ContentStartOffset)
{
Invariant.Assert(sTextRange.Start + sTextRange.Length > textMap.ContentStartOffset);
// We've found an error that start in the context and extends into
// the content. This can happen as more text is revealed to the
// speller engine as the caret moves forward.
// E.g., while scanning "avalon's" we flag an error over "avalon",
// ignoring the "'s" because the caret is positioned within that segment.
// Then, the user hits space and now we analyze "'s" along with its
// preceding context "avalon". In this final scan, "avalon's" as a while
// is flagged as an error and we enter this if statement.
// We must mark the range clean before we can mark it dirty.
// _statusTable.MarkErrorRange can only handle clean runs.
_statusTable.MarkCleanRange(errorStart, errorEnd);
}
_statusTable.MarkErrorRange(errorStart, errorEnd);
}
// Examines a position and returns to two relative positions:
//
// contentPosition -> position moved inward to the nearest word
// break (opposite direction param, toward the content).
//
// contextPosition -> position moved outward away from content
// to a word break that includes sufficient text to handle multi-
// word errors correctly.
///
/// Critical - it calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a trusted parameter, that doesn't come from untrusted sources.
///
[SecurityCritical, SecurityTreatAsSafe]
private void ExpandToWordBreakAndContext(ITextPointer position, LogicalDirection direction, XmlLanguage language,
out ITextPointer contentPosition, out ITextPointer contextPosition)
{
ITextPointer start;
ITextPointer end;
ITextPointer outwardPosition;
ITextPointer inwardPosition;
TextMap textMap;
ArrayList segments;
SpellerInterop.STextRange sTextRange;
LogicalDirection inwardDirection;
int i;
contentPosition = position;
contextPosition = position;
if (position.GetPointerContext(direction) == TextPointerContext.None)
{
// There is no following context, we're at document start/end.
return;
}
// Disable spell checking functionality since we're only
// interested in word breaks here. This greatly cuts down
// the engine's workload.
_spellerInterop.SetContextOption("IsSpellChecking", false);
//
// Build an array of wordbreak offsets surrounding the position.
//
// 1. Search outward, into surrounding text. We need MinWordBreaksForContext
// word breaks to handle multi-word errors.
outwardPosition = SearchForWordBreaks(position, direction, language, MinWordBreaksForContext, true /* stopOnError */);
// 2. Search inward, towards content. We just need one word break inward.
inwardDirection = direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward;
inwardPosition = SearchForWordBreaks(position, inwardDirection, language, 1, false /* stopOnError */);
// Get combined word breaks. This may not be the same as we calculated
// in two parts above, since we don't know yet whether or not position is
// on a word break.
if (direction == LogicalDirection.Backward)
{
start = outwardPosition;
end = inwardPosition;
}
else
{
start = inwardPosition;
end = outwardPosition;
}
textMap = new TextMap(start, end, position, position);
segments = new ArrayList(MinWordBreaksForContext + 1);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ExpandToWordBreakCallback), segments);
//
// Use our table of word breaks to calculate context and content positions.
//
if (segments.Count == 0)
{
// No segments. This can happen if position is surrounded by
// nothing but white space. We've already initialized contentPosition
// and contextPosition so there's nothing to do.
}
else
{
int leftWordBreak;
int rightWordBreak;
int contentOffset;
int contextOffset;
// Figure out where position lives in the segment list.
i = FindPositionInSegmentList(textMap, direction, segments, out leftWordBreak, out rightWordBreak);
// contentPosition should be an edge on the segment we found.
if (direction == LogicalDirection.Backward)
{
contentOffset = textMap.ContentStartOffset == rightWordBreak ? rightWordBreak : leftWordBreak;
}
else
{
contentOffset = textMap.ContentStartOffset == leftWordBreak ? leftWordBreak : rightWordBreak;
}
contentPosition = textMap.MapOffsetToPosition(contentOffset);
// contextPosition should be MinWordBreaksForContext - 1 words away.
if (direction == LogicalDirection.Backward)
{
i -= (MinWordBreaksForContext - 1);
sTextRange = (SpellerInterop.STextRange)segments[Math.Max(i, 0)];
// We might actually follow contentOffset if we're at the document edge.
// Don't let that happen.
contextOffset = Math.Min(sTextRange.Start, contentOffset);
}
else
{
i += MinWordBreaksForContext;
sTextRange = (SpellerInterop.STextRange)segments[Math.Min(i, segments.Count-1)];
// We might actually preceed contentOffset if we're at the document edge.
// Don't let that happen.
contextOffset = Math.Max(sTextRange.Start + sTextRange.Length, contentOffset);
}
contextPosition = textMap.MapOffsetToPosition(contextOffset);
}
// Final fixup: if the dirty range covers only formatting (which is not passed
// to the speller engine) then we might actually "expand" in the wrong
// direction, since the TextMap will jump over formatting.
// Backup if necessary.
if (direction == LogicalDirection.Backward)
{
if (position.CompareTo(contentPosition) < 0)
{
contentPosition = position;
}
if (position.CompareTo(contextPosition) < 0)
{
contextPosition = position;
}
}
else
{
if (position.CompareTo(contentPosition) > 0)
{
contentPosition = position;
}
if (position.CompareTo(contextPosition) > 0)
{
contextPosition = position;
}
}
}
// Helper for ExpandToWordBreakAndContext -- returns the index of a segment
// containing or bordering a specified position (textMap.ContentStartOffset).
// Also returns the offset, within the TextMap, of the two word breaks surrounding
// TextMap.ContentStartOffset. The word breaks may be segment edges, or
// the extent of a run of whitespace between two segments.
private int FindPositionInSegmentList(TextMap textMap, LogicalDirection direction, ArrayList segments,
out int leftWordBreak, out int rightWordBreak)
{
SpellerInterop.STextRange sTextRange;
int index;
// Make the compiler happy by initializing the out's to bogus values.
leftWordBreak = Int32.MaxValue;
rightWordBreak = -1;
// Check before the first segment, which start at the first
// non-whitespace char.
sTextRange = (SpellerInterop.STextRange)segments[0];
if (textMap.ContentStartOffset < sTextRange.Start)
{
leftWordBreak = 0;
rightWordBreak = sTextRange.Start;
index = -1;
}
else
{
// Check after the last segment, which does not include final whitespace.
sTextRange = (SpellerInterop.STextRange)segments[segments.Count-1];
if (textMap.ContentStartOffset > sTextRange.Start + sTextRange.Length)
{
leftWordBreak = sTextRange.Start + sTextRange.Length;
rightWordBreak = textMap.TextLength;
index = segments.Count;
}
else
{
// Walk the segment list, checking each segment and space in between.
for (index = 0; index < segments.Count; index++)
{
sTextRange = (SpellerInterop.STextRange)segments[index];
leftWordBreak = sTextRange.Start;
rightWordBreak = sTextRange.Start + sTextRange.Length;
// Check if we're inside this segment.
if (leftWordBreak <= textMap.ContentStartOffset &&
rightWordBreak >= textMap.ContentStartOffset)
{
break;
}
// Or if we're between this segment and the next one --
// segments do not include white space.
if (index < segments.Count - 1 &&
rightWordBreak < textMap.ContentStartOffset)
{
sTextRange = (SpellerInterop.STextRange)segments[index + 1];
leftWordBreak = rightWordBreak;
rightWordBreak = sTextRange.Start;
if (rightWordBreak > textMap.ContentStartOffset)
{
// position is between segments[i] and segments[i+1].
// Adjust i so that adding MinWordBreaksForContext below
// doesn't include an extra word.
if (direction == LogicalDirection.Backward)
{
index++;
}
break;
}
}
}
}
}
Invariant.Assert(leftWordBreak <= textMap.ContentStartOffset && textMap.ContentStartOffset <= rightWordBreak);
return index;
}
// Helper for ExpandToWordBreakAndContext -- returns the position
// of the nth word break in the specified direction.
// If stopOnError is true, the search will halt if an error run is
// encountered along the way. The search is also halted if text in
// a new language is encountered.
///
/// Critical - it calls EnumTextSegments(), which is Critical.
/// TreatAsSafe - it calls it passing a TextMap object constructed appropriately in this method.
///
[SecurityCritical, SecurityTreatAsSafe]
private ITextPointer SearchForWordBreaks(ITextPointer position, LogicalDirection direction, XmlLanguage language, int minWordCount, bool stopOnError)
{
ITextPointer closestErrorPosition;
ITextPointer searchPosition;
ITextPointer start;
ITextPointer end;
StaticTextPointer nextErrorTransition;
int segmentCount;
TextMap textMap;
searchPosition = position.CreatePointer();
closestErrorPosition = null;
if (stopOnError)
{
nextErrorTransition = _statusTable.GetNextErrorTransition(position.CreateStaticPointer(), direction);
if (!nextErrorTransition.IsNull)
{
closestErrorPosition = nextErrorTransition.CreateDynamicTextPointer(LogicalDirection.Forward);
}
}
bool hitBreakPoint = false;
do
{
searchPosition.MoveByOffset(direction == LogicalDirection.Backward ? -ContextBlockSize : +ContextBlockSize);
// Don't go past closestErrorPosition.
if (closestErrorPosition != null)
{
if (direction == LogicalDirection.Backward && closestErrorPosition.CompareTo(searchPosition) > 0 ||
direction == LogicalDirection.Forward && closestErrorPosition.CompareTo(searchPosition) < 0)
{
searchPosition.MoveToPosition(closestErrorPosition);
hitBreakPoint = true;
}
}
// Don't venture into text in another language.
ITextPointer closestLanguageTransition = GetNextLanguageTransition(position, direction, language, searchPosition);
if (direction == LogicalDirection.Backward && closestLanguageTransition.CompareTo(searchPosition) > 0 ||
direction == LogicalDirection.Forward && closestLanguageTransition.CompareTo(searchPosition) < 0)
{
searchPosition.MoveToPosition(closestLanguageTransition);
hitBreakPoint = true;
}
if (direction == LogicalDirection.Backward)
{
start = searchPosition;
end = position;
}
else
{
start = position;
end = searchPosition;
}
textMap = new TextMap(start, end, start, end);
segmentCount = _spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null, null, null);
}
while (!hitBreakPoint &&
segmentCount < minWordCount + 1 &&
searchPosition.GetPointerContext(direction) != TextPointerContext.None);
return searchPosition;
}
// Returns the closest of either a halting position or the position preceding text
// tagged with a differing XmlLanguage from a start position.
private ITextPointer GetNextLanguageTransition(ITextPointer position, LogicalDirection direction, XmlLanguage language, ITextPointer haltPosition)
{
ITextPointer navigator = position.CreatePointer();
while ((direction == LogicalDirection.Forward && navigator.CompareTo(haltPosition) < 0) ||
(direction == LogicalDirection.Backward && navigator.CompareTo(haltPosition) > 0))
{
if ((XmlLanguage)navigator.GetValue(FrameworkElement.LanguageProperty) != language)
break;
navigator.MoveToNextContextPosition(direction);
}
// If we moved past haltPosition on the final MoveToNextContextPosition, move back.
if ((direction == LogicalDirection.Forward && navigator.CompareTo(haltPosition) > 0) ||
(direction == LogicalDirection.Backward && navigator.CompareTo(haltPosition) < 0))
{
navigator.MoveToPosition(haltPosition);
}
return navigator;
}
// Called indirectly by ExpandToWordBreakAndContext while iterating segments.
// Builds up an array of segment offsets while iterating.
private bool ExpandToWordBreakCallback(object textSegment, object o)
{
ArrayList segments = (ArrayList)o;
SpellerInterop.STextRange sTextRange = SpellerInterop.GetSegmentRange(textSegment);
segments.Add(sTextRange);
return true;
}
// Returns true if a user has tagged the specified word with "Ignore All".
private bool IsIgnoredWord(char[] word)
{
bool isIgnoredWord = false;
if (_ignoredWordsList != null)
{
isIgnoredWord = _ignoredWordsList.BinarySearch(new string(word), new CaseInsensitiveComparer(_defaultCulture)) >= 0;
}
return isIgnoredWord;
}
// Returns true if we have an engine capable of proofing the specified
// language.
private static bool CanSpellCheck(CultureInfo culture)
{
bool canSpellCheck;
switch (culture.TwoLetterISOLanguageName)
{
case "en":
case "de":
case "fr":
case "es":
canSpellCheck = true;
break;
default:
canSpellCheck = false;
break;
}
return canSpellCheck;
}
// Sets the speller engine language and spelling reform options.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private void SetCulture(CultureInfo culture)
{
//
// Set the language.
//
_spellerInterop.SetLocale(culture.LCID);
//
// Set spelling reform, if necessary.
//
string option;
// The engine only accepts values for german and french.
switch (culture.TwoLetterISOLanguageName)
{
case "de":
option = "GermanReform";
break;
case "fr":
option = "FrenchReform";
break;
default:
option = null;
break;
}
if (option != null)
{
switch (_spellingReform)
{
case SpellingReform.Prereform:
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Prereform);
break;
case SpellingReform.Postreform:
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Postreform);
break;
case SpellingReform.PreAndPostreform:
if (option == "GermanReform")
{
// BothPreAndPost is disallowed for german -- the engine has undefined results.
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Postreform);
}
else
{
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.BothPreAndPost);
}
break;
}
}
}
// Scans the word containing a specified character.
private void ScanPosition(ITextPointer position, LogicalDirection direction)
{
ITextPointer start;
ITextPointer end;
if (direction == LogicalDirection.Forward)
{
start = position;
end = position.CreatePointer(+1);
}
else
{
start = position.CreatePointer(-1);
end = position;
}
XmlLanguage language = (XmlLanguage)position.GetValue(FrameworkElement.LanguageProperty);
ScanRange(start, end, Int64.MaxValue /* timeLimit */);
}
// Returns the CultureInfo of the content at a position.
// Returns null if there is no CultureInfo matching the current XmlLanguage.
private CultureInfo GetCurrentCultureAndLanguage(ITextPointer position, out XmlLanguage language)
{
CultureInfo cultureInfo;
bool hasModifiers;
// TextBox takes the input language iff no local LanguageProperty is set.
if (!_textEditor.AcceptsRichContent &&
_textEditor.UiScope.GetValueSource(FrameworkElement.LanguageProperty, null, out hasModifiers) == BaseValueSourceInternal.Default)
{
cultureInfo = _defaultCulture;
language = XmlLanguage.GetLanguage(cultureInfo.IetfLanguageTag);
}
else
{
language = (XmlLanguage)position.GetValue(FrameworkElement.LanguageProperty);
if (language == null)
{
cultureInfo = null;
}
else
{
try
{
cultureInfo = language.GetSpecificCulture();
}
catch (InvalidOperationException)
{
// Someone set a bogus language on the run.
cultureInfo = null;
}
}
}
return cultureInfo;
}
#endregion Private methods
//-----------------------------------------------------
//
// Private Types
//
//-----------------------------------------------------
#region Private Types
// Holds a run of text intended for the speller engine.
// Because the engine only understands plain text, this class coverts
// arbitrary runs of document text to speller-suitable plain text
// and keeps a table that allows it to efficiently map back from plain
// text offsets (used by the engine) to ITextPointers.
private class TextMap
{
// Creates a new instance.
// contextStart/End refer to the whole run of text.
// contentStart/End are a subset of the text, which is what
// the engine will actually tag with errors.
// The space between context and content is used by the engine
// to correctly analyze multiple word phrase like "Los Angeles"
// that could otherwise be truncated and incorrectly tagged.
internal TextMap(ITextPointer contextStart, ITextPointer contextEnd,
ITextPointer contentStart, ITextPointer contentEnd)
{
ITextPointer position;
int maxChars;
int inlineCount;
int runCount;
int i;
int distance;
Invariant.Assert(contextStart.CompareTo(contentStart) <= 0);
Invariant.Assert(contextEnd.CompareTo(contentEnd) >= 0);
_basePosition = contextStart.GetFrozenPointer(LogicalDirection.Backward);
position = contextStart.CreatePointer();
maxChars = contextStart.GetOffsetToPosition(contextEnd);
_text = new char[maxChars];
_positionMap = new int[maxChars+1];
_textLength = 0;
inlineCount = 0;
_contentStartOffset = 0;
_contentEndOffset = 0;
// Iterate over the run, building up a matching plain text buffer
// and a table that tells us how to map back to the original text.
while (position.CompareTo(contextEnd) < 0)
{
if (position.CompareTo(contentStart) == 0)
{
_contentStartOffset = _textLength;
}
if (position.CompareTo(contentEnd) == 0)
{
_contentEndOffset = _textLength;
}
switch (position.GetPointerContext(LogicalDirection.Forward))
{
case TextPointerContext.Text:
runCount = position.GetTextRunLength(LogicalDirection.Forward);
runCount = Math.Min(runCount, _text.Length - _textLength);
runCount = Math.Min(runCount, position.GetOffsetToPosition(contextEnd));
position.GetTextInRun(LogicalDirection.Forward, _text, _textLength, runCount);
for (i = _textLength; i < _textLength + runCount; i++)
{
_positionMap[i] = i + inlineCount;
}
distance = position.GetOffsetToPosition(contentStart);
if (distance >= 0 && distance <= runCount)
{
_contentStartOffset = _textLength + position.GetOffsetToPosition(contentStart);
}
distance = position.GetOffsetToPosition(contentEnd);
if (distance >= 0 && distance <= runCount)
{
_contentEndOffset = _textLength + position.GetOffsetToPosition(contentEnd);
}
position.MoveByOffset(runCount);
_textLength += runCount;
break;
case TextPointerContext.ElementStart:
case TextPointerContext.ElementEnd:
if (IsAdjacentToFormatElement(position))
{
// Filter out formatting tags from the plain text.
inlineCount++;
}
else
{
// Stick in a word break to account for the block element.
_text[_textLength] = ' ';
_positionMap[_textLength] = _textLength + inlineCount;
_textLength++;
}
position.MoveToNextContextPosition(LogicalDirection.Forward);
break;
case TextPointerContext.EmbeddedElement:
_text[_textLength] = '\xf8ff'; // Unicode private use.
_positionMap[_textLength] = _textLength + inlineCount;
_textLength++;
position.MoveToNextContextPosition(LogicalDirection.Forward);
break;
}
}
if (position.CompareTo(contentEnd) == 0)
{
_contentEndOffset = _textLength;
}
if (_textLength > 0)
{
_positionMap[_textLength] = _positionMap[_textLength - 1] + 1;
}
else
{
_positionMap[0] = 0;
}
Invariant.Assert(_contentStartOffset <= _contentEndOffset);
}
// Returns an ITextPointer in the document with position matching
// an offset within the plain text.
internal ITextPointer MapOffsetToPosition(int offset)
{
Invariant.Assert(offset >= 0 && offset <= _textLength);
return _basePosition.CreatePointer(_positionMap[offset]);
}
// Offset in the plain text of the content start.
internal int ContentStartOffset
{
get { return _contentStartOffset; }
}
// Offset in the plain text of the content end.
internal int ContentEndOffset
{
get { return _contentEndOffset; }
}
// Plain text representation of the document run.
// Do not use Text.Length! The actual content size may be smaller,
// use the TextLength property instead.
internal char[] Text
{
get { return _text; }
}
// Length of the plain text. This may be less than Text.Length --
// we allocate a maximum value for the array which is not always used
// by the final content.
internal int TextLength
{
get { return _textLength; }
}
// Returns true if pointer preceeds an Inline start or end edge.
private bool IsAdjacentToFormatElement(ITextPointer pointer)
{
TextPointerContext context;
bool isAdjacentToFormatElement;
isAdjacentToFormatElement = false;
context = pointer.GetPointerContext(LogicalDirection.Forward);
if (context == TextPointerContext.ElementStart &&
TextSchema.IsFormattingType(pointer.GetElementType(LogicalDirection.Forward)))
{
isAdjacentToFormatElement = true;
}
else if (context == TextPointerContext.ElementEnd &&
TextSchema.IsFormattingType(pointer.ParentType))
{
isAdjacentToFormatElement = true;
}
return isAdjacentToFormatElement;
}
// Position of the plain text block within the document.
private readonly ITextPointer _basePosition;
// Plain text version of the document run.
private readonly char[] _text;
// Map of plain text offsets to document symbol offsets relative
// to _basePosition.
private readonly int[] _positionMap;
// Size of the content within _text.
private readonly int _textLength;
// Plain text offset of the content start.
private readonly int _contentStartOffset;
// Plain text offset of the content end.
private readonly int _contentEndOffset;
}
// Holds state tracking the progress of speller scan,
// used during idle time document analysis.
private class ScanStatus
{
// Creates a new instance. timeLimit is the maximum value of
// DateTime.Now.Ticks at which the scan should end.
internal ScanStatus(long timeLimit)
{
_timeLimit = timeLimit;
}
// Returns true if the scan has exceeded its time budget.
internal bool HasExceededTimeLimit
{
get
{
long nowTicks = DateTime.Now.Ticks;
#if DEBUG
// Track how far over budget we are the first time we check.
if (nowTicks >= _timeLimit && _debugMsOverTimeLimit == 0)
{
_debugMsOverTimeLimit = (int)(((double)(nowTicks - _timeLimit)) / 10000);
}
#endif // DEBUG
return nowTicks >= _timeLimit;
}
}
// If we've timed out, holds the position we left off -- the remainder
// of the text run yet to be analyzed.
internal ITextPointer TimeoutPosition
{
get { return _timeoutPosition; }
set { _timeoutPosition = value; }
}
// Budget for this scan, in 100 nanosecond intervals.
private readonly long _timeLimit;
// If we've timed out, holds the position we left off -- the remainder
// of the text run yet to be analyzed.
private ITextPointer _timeoutPosition;
#if DEBUG
// Number of milliseconds we've exceeded our time limit by.
private int _debugMsOverTimeLimit;
#endif // DEBUG
}
// Container used to hold state for SpellerInterop callbacks.
private class TextMapCallbackData
{
internal TextMapCallbackData(TextMap textmap, object data)
{
_textmap = textmap;
_data = data;
}
internal TextMap TextMap { get { return _textmap; } }
internal object Data { get { return _data; } }
private readonly TextMap _textmap;
private readonly object _data;
}
#endregion Private Types
//-----------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
// Max time slice for speller background proofing, in milliseconds.
// Larger numbers mean we can scan entire documents more quickly,
// but with less responsiveness to user interruptions.
private const int MaxIdleTimeSliceMs = 20;
// Max time slice for speller background proofing, in 100 nanosecond intervals.
private const long MaxIdleTimeSliceNs = MaxIdleTimeSliceMs*10000;
// Max number of characters to pass to engine in a single call.
// Increasing this number will decrease the time to scan an entire
// document, at the cost of more time spent in individual Idle callbacks
// (app is less respsonsive).
// NLG devs have warned us not to set this value below 32, to avoid
// cases where they don't have enough context to identify errors
// correctly.
private const int MaxScanBlockSize = 64;
// Number of characters to advance on each iteration while searching
// for context. We need at least three words on either side of a text
// run to give the speller enough context to identify multi-word
// errors (or non-errors, like Los Angeles).
//
// Larger numbers here will mean fewer text scans, but a smaller
// minimum scan.
private const int ContextBlockSize = 32;
// The minimum number of word breaks we need to have sufficient
// context for detecting multi-word errors. This number is
// a constant -- it is not something to adjust for perf.
private const int MinWordBreaksForContext = 4;
// TextEditor that owns this Speller.
private TextEditor _textEditor;
// A run-length array tracking speller status of all text in the document.
private SpellerStatusTable _statusTable;
// HighlightLayer used to display error squiggles.
private SpellerHighlightLayer _highlightLayer;
// Engine object used to analyze runs of text.
// kepowell from the nlg team suggests that we cache a single
// ITextChunk/ITextContext for the thread and reuse it across
// Spellers. FE TextChunks in particular are expensive, because
// they cache large amounts of data per instance, on the order
// of 10k's of data.
///
/// Critical: This object could expose a COM object which can run code under elevation
///
[SecurityCritical]
private SpellerInterop _spellerInterop;
// Current spelling reform setting.
private SpellingReform _spellingReform;
// true if we've already posted but not yet received a background queue item.
private bool _pendingIdleCallback;
// true if we have an active TextSelection.Changed listener.
private bool _pendingCaretMovedCallback;
// List of words tagged by the user as non-errors.
private ArrayList _ignoredWordsList;
// The CultureInfo associated with this speller.
// Used for ignored words comparison, and plain text controls (TextBox).
private readonly CultureInfo _defaultCulture;
// Set true if the nl6 library is unavailable.
private bool _failedToInit;
#endregion Private Fields
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------------------
//
// File: Speller.cs
//
// Description: Spell checking component for the TextEditor.
//
//---------------------------------------------------------------------------
namespace System.Windows.Documents
{
using MS.Internal;
using System.Threading;
using System.Windows.Threading;
using System.Globalization;
using System.Collections;
using System.Security;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using MS.Win32;
using MS.Internal.PresentationFramework;
using System.Windows.Controls;
using System.Windows.Markup; // XmlLanguage
using System.Windows.Input;
// Spell checking component for the TextEditor.
internal class Speller
{
//-----------------------------------------------------
//
// Constructors
//
//-----------------------------------------------------
#region Constructors
// Creates a new instance. We have at most one Speller instance
// per TextEditor.
internal Speller(TextEditor textEditor)
{
_textEditor = textEditor;
_textEditor.TextContainer.Change += new TextContainerChangeEventHandler(OnTextContainerChange);
// Schedule some idle time to start examining the document.
if (_textEditor.TextContainer.SymbolCount > 0)
{
ScheduleIdleCallback();
}
_defaultCulture = InputLanguageManager.Current != null ? InputLanguageManager.Current.CurrentInputLanguage :
Thread.CurrentThread.CurrentCulture;
}
#endregion Constructors
//------------------------------------------------------
//
// Internal Methods
//
//-----------------------------------------------------
#region Internal Methods
// Called by TextEditor to disable spelling.
///
/// Critical - this code resets the _textChunk member which is critical
/// TreatAsSafe - the operation is safe to expose
///
[SecurityCritical, SecurityTreatAsSafe]
internal void Detach()
{
Invariant.Assert(_textEditor != null);
_textEditor.TextContainer.Change -= new TextContainerChangeEventHandler(OnTextContainerChange);
if (_pendingCaretMovedCallback)
{
_textEditor.Selection.Changed -= new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus -= new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = false;
}
// Shutdown the highlight layer.
if (_highlightLayer != null)
{
_textEditor.TextContainer.Highlights.RemoveLayer(_highlightLayer);
_highlightLayer = null;
}
// Shutdown the status table.
_statusTable = null;
// Release our nl6 objects.
if (_spellerInterop != null)
{
_spellerInterop.Dispose();
_spellerInterop = null;
}
// Clear the TextEditor. (Used as a sentinel to track Detachedness
// from pending idle callback.)
_textEditor = null;
}
// Returns an object holding state about an error at the specified
// position, or null if no error is present.
//
// If forceEvaluation is set true, the speller will analyze any dirty region
// covered by the position. Otherwise dirty regions will be treated as
// non-errors.
internal SpellingError GetError(ITextPointer position, LogicalDirection direction, bool forceEvaluation)
{
ITextPointer start;
ITextPointer end;
SpellingError error;
// Evaluate any pending dirty region.
if (forceEvaluation &&
EnsureInitialized() &&
_statusTable.IsRunType(position.CreateStaticPointer(), direction, SpellerStatusTable.RunType.Dirty))
{
ScanPosition(position, direction);
}
// Get the error result.
if (_statusTable != null &&
_statusTable.GetError(position.CreateStaticPointer(), direction, out start, out end))
{
error = new SpellingError(this, start, end);
}
else
{
error = null;
}
return error;
}
// Worker for TextBox/RichTextBox.GetNextSpellingErrorPosition.
// Returns the start position of the next error, or null if no error exists.
//
// NB: this method will force an evaluation of any dirty regions between
// position and the next error, which in the worst case is the rest of
// the document.
internal ITextPointer GetNextSpellingErrorPosition(ITextPointer position, LogicalDirection direction)
{
if (!EnsureInitialized())
return null;
StaticTextPointer scanPosition = position.CreateStaticPointer();
StaticTextPointer endPosition;
SpellerStatusTable.RunType runType;
while (_statusTable.GetRun(scanPosition, direction, out runType, out endPosition))
{
if (runType == SpellerStatusTable.RunType.Error)
break;
if (runType == SpellerStatusTable.RunType.Dirty)
{
ScanPosition(scanPosition.CreateDynamicTextPointer(direction), direction);
_statusTable.GetRun(scanPosition, direction, out runType, out endPosition);
Invariant.Assert(runType != SpellerStatusTable.RunType.Dirty);
if (runType == SpellerStatusTable.RunType.Error)
break;
}
scanPosition = endPosition;
}
SpellingError spellingError = GetError(scanPosition.CreateDynamicTextPointer(direction), direction, false /* forceEvaluation */);
return spellingError == null ? null : spellingError.Start;
}
// Called by SpellingError to retreive a list of suggestions
// for an error range.
// This method actually runs the speller on the specified text,
// re-evaluating the error from scratch.
///
/// Critical - It calls SetContextOption() which is Critical.
/// TreatAsSafe - it calls them with trusted parameters.
///
[SecurityCritical, SecurityTreatAsSafe]
internal IList GetSuggestionsForError(SpellingError error)
{
ITextPointer contextStart;
ITextPointer contextEnd;
ITextPointer contentStart;
ITextPointer contentEnd;
TextMap textMap;
ArrayList suggestions;
suggestions = new ArrayList(1);
//
// IMPORTANT!!
//
// This logic here must match ScanRange, or else we might not
// calculate the exact same error. Keep the two methods in [....]!
//
XmlLanguage language;
CultureInfo culture = GetCurrentCultureAndLanguage(error.Start, out language);
if (culture == null || !CanSpellCheck(culture))
{
// Return an empty list.
}
else
{
ExpandToWordBreakAndContext(error.Start, LogicalDirection.Backward, language, out contentStart, out contextStart);
ExpandToWordBreakAndContext(error.End, LogicalDirection.Forward, language, out contentEnd, out contextEnd);
textMap = new TextMap(contextStart, contextEnd, contentStart, contentEnd);
SetCulture(culture);
_spellerInterop.SetContextOption("IsSpellChecking", true);
_spellerInterop.SetContextOption("IsSpellVerifyOnly", false);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ScanErrorTextSegment), new TextMapCallbackData(textMap, suggestions));
}
return suggestions;
}
// Worker for context menu's "Ignore All" item.
// Adds a word to the ignore list, and clears any matching errors.
//
//
internal void IgnoreAll(string word)
{
if (_ignoredWordsList == null)
{
_ignoredWordsList = new ArrayList(1);
}
int index = _ignoredWordsList.BinarySearch(word, new CaseInsensitiveComparer(_defaultCulture));
if (index < 0)
{
// This is a new word to ignore.
// Add it the list so we don't flag it later.
_ignoredWordsList.Insert(~index, word);
// Then search through the error list, clearing any matching
// errors.
if (_statusTable != null)
{
StaticTextPointer pointer = _textEditor.TextContainer.CreateStaticPointerAtOffset(0);
ITextPointer errorStart;
ITextPointer errorEnd;
Char[] charArray = null;
while (!pointer.IsNull)
{
if (_statusTable.GetError(pointer, LogicalDirection.Forward, out errorStart, out errorEnd))
{
string error = TextRangeBase.GetTextInternal(errorStart, errorEnd, ref charArray);
if (String.Compare(word, error, true /* ignoreCase */, _defaultCulture) == 0)
{
_statusTable.MarkCleanRange(errorStart, errorEnd);
}
}
pointer = _statusTable.GetNextErrorTransition(pointer, LogicalDirection.Forward);
}
}
}
}
// Sets the speller engine spelling reform option.
internal void SetSpellingReform(SpellingReform spellingReform)
{
if (_spellingReform != spellingReform)
{
_spellingReform = spellingReform;
// Invalidate the whole document.
ResetErrors();
}
}
// Called when a global state change invalidates all cached errors.
internal void ResetErrors()
{
if (_statusTable != null)
{
_statusTable.MarkDirtyRange(_textEditor.TextContainer.Start, _textEditor.TextContainer.End);
if (_textEditor.TextContainer.SymbolCount > 0)
{
ScheduleIdleCallback();
}
}
}
// Returns true if the specified property affects speller evaluation.
internal static bool IsSpellerAffectingProperty(DependencyProperty property)
{
return property == FrameworkElement.LanguageProperty ||
property == SpellCheck.SpellingReformProperty;
}
#endregion Internal methods
//------------------------------------------------------
//
// Internal Properties
//
//------------------------------------------------------
#region Internal Properties
// A run-length array tracking speller status of all text in the document.
internal SpellerStatusTable StatusTable
{
get
{
return _statusTable;
}
}
#endregion Internal Properties
//-----------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// Initializes state for the Speller.
// Delayed until the first text change event, or first idle callback.
///
/// Critical - This code calls into _textchunk and other unmanaged COM api.
/// TreatAsSafe - critical operations are not based on untrusted input. multiple calls don't involve any risk.
///
[SecurityCritical, SecurityTreatAsSafe]
private bool EnsureInitialized()
{
if (_spellerInterop != null)
return true;
if (_failedToInit)
return false;
Invariant.Assert(_highlightLayer == null);
Invariant.Assert(_statusTable == null);
try
{
_spellerInterop = new SpellerInterop();
}
catch (DllNotFoundException)
{
_failedToInit = true;
}
catch (EntryPointNotFoundException)
{
_failedToInit = true;
}
if (_failedToInit)
return false;
_highlightLayer = new SpellerHighlightLayer(this);
_statusTable = new SpellerStatusTable(_textEditor.TextContainer.Start, _highlightLayer);
_textEditor.TextContainer.Highlights.AddLayer(_highlightLayer);
//
_spellerInterop.SetContextOption("IsSpellSuggestingMWEs", false);
_spellingReform = (SpellingReform)_textEditor.UiScope.GetValue(SpellCheck.SpellingReformProperty);
return true;
}
// Posts a background priority operation to the dispatcher queue.
// All scanning takes place during idle-time callbacks.
private void ScheduleIdleCallback()
{
if (!_pendingIdleCallback)
{
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new DispatcherOperationCallback(OnIdle), null);
_pendingIdleCallback = true;
}
}
// Enables the TextSelection.Changed listener.
// We call this method when an otherwise clean document has text
// covered by the caret or an IME composition that must be analyzed
// when the selection moves away.
private void ScheduleCaretMovedCallback()
{
if (!_pendingCaretMovedCallback)
{
_textEditor.Selection.Changed += new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus += new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = true;
}
}
// Callback for document changes.
// Marks appropriate sections of the document as dirty then posts
// an idle request for future analysis.
private void OnTextContainerChange(object sender, TextContainerChangeEventArgs e)
{
Invariant.Assert(sender == _textEditor.TextContainer);
if (e.Count == 0 ||
(e.TextChange == TextChangeType.PropertyModified && !IsSpellerAffectingProperty(e.Property)))
{
// Speller doesn't care about most property changes.
return;
}
if (_failedToInit)
{
// Speller engine is not available.
return;
}
if (_statusTable != null)
{
_statusTable.OnTextChange(e);
}
ScheduleIdleCallback();
}
// Runs the speller idle callback.
// During this callback, we scan dirty portions of the document
// until all text is examined, or we exceed a set time limit.
// If we run out of time with more work to do, we post a new idle
// callback request, yielding to any pending high-priority work
// (such as user input).
//
private object OnIdle(object unused)
{
Invariant.Assert(_pendingIdleCallback);
// Reset _pendingIdleCallback.
_pendingIdleCallback = false;
// _textEditor will be null if we've been detached since requesting the callback.
if (_textEditor != null &&
EnsureInitialized())
{
ITextPointer start;
ITextPointer end;
long timeLimit;
ScanStatus status;
timeLimit = DateTime.Now.Ticks + MaxIdleTimeSliceNs;
end = null;
status = null;
// Iterate over chunks of dirty text until we run out of time
// or finish with the entire document.
do
{
if (!GetNextScanRange(end, out start, out end))
break;
status = ScanRange(start, end, timeLimit);
}
while (!status.HasExceededTimeLimit);
// Schedule any pending work before we yield.
if (status != null)
{
if (status.HasExceededTimeLimit)
{
ScheduleIdleCallback();
}
}
}
return null;
}
// Callback for TextSelection.Changed event.
private void OnCaretMoved(object sender, EventArgs e)
{
OnCaretMovedWorker();
}
// Callback for UiScope.LostFocus event.
private void OnLostFocus(object sender, RoutedEventArgs e)
{
OnCaretMovedWorker();
}
// Callback for the TextSelection.Changed or UiScope.LostFocus events.
// We enter this method when an otherwise clean document has text
// covered by the caret or an IME composition that must be analyzed
// when the selection moves away.
private void OnCaretMovedWorker()
{
if (!_pendingCaretMovedCallback || _textEditor == null)
{
// Because the event route caches the callback, we can get a
// callback even after removing the handler.
// Just ignore the spurious callback.
return;
}
_textEditor.Selection.Changed -= new EventHandler(OnCaretMoved);
_textEditor.UiScope.LostFocus -= new RoutedEventHandler(OnLostFocus);
_pendingCaretMovedCallback = false;
// Now that the caret's out of the way, analyze the text it
// used to cover the next time the app goes idle.
ScheduleIdleCallback();
}
// Calculates the next run of text to feed to the speller.
// If there's nothing left to analyze, returns false and start/end will
// be null on exit.
private bool GetNextScanRange(ITextPointer searchStart, out ITextPointer start, out ITextPointer end)
{
ITextPointer rawStart;
ITextPointer rawEnd;
start = null;
end = null;
//
// First iteration of the scan loop, searchStart == null.
if (searchStart == null)
{
searchStart = _textEditor.TextContainer.Start;
}
// Grab the first dirty range.
GetNextScanRangeRaw(searchStart, out rawStart, out rawEnd);
if (rawStart != null)
{
// Skip over the caret and/or IME composition.
AdjustScanRangeAroundComposition(rawStart, rawEnd, out start, out end);
}
return start != null;
}
// Finds the next dirty range following searchStart, without considering
// the current caret or IME composition.
private void GetNextScanRangeRaw(ITextPointer searchStart, out ITextPointer start, out ITextPointer end)
{
Invariant.Assert(searchStart != null);
start = null;
end = null;
// Grab the first dirty range.
_statusTable.GetFirstDirtyRange(searchStart, out start, out end);
if (start != null)
{
Invariant.Assert(start.CompareTo(end) < 0);
// Cap the block size by a constant.
if (start.GetOffsetToPosition(end) > MaxScanBlockSize)
{
end = start.CreatePointer(MaxScanBlockSize);
}
// Ensure the block has constant language.
XmlLanguage language = (XmlLanguage)start.GetValue(FrameworkElement.LanguageProperty);
end = GetNextLanguageTransition(start, LogicalDirection.Forward, language, end);
Invariant.Assert(start.CompareTo(end) < 0);
}
}
// Truncates a range of text if it overlaps the word containing the
// caret, or an IME composition.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private void AdjustScanRangeAroundComposition(ITextPointer rawStart, ITextPointer rawEnd,
out ITextPointer start, out ITextPointer end)
{
start = rawStart;
end = rawEnd;
if (!_textEditor.Selection.IsEmpty)
{
// No caret to adjust around.
return;
}
if (!_textEditor.UiScope.IsKeyboardFocused)
{
// Document isn't focused, no caret rendered.
return;
}
// Get the word surrounding the caret.
ITextPointer wordBreakLeft;
ITextPointer wordBreakRight;
ITextPointer caretPosition;
TextMap textMap;
ArrayList segments;
caretPosition = _textEditor.Selection.Start;
// Disable spell checking functionality since we're only
// interested in word breaks here. This greatly cuts down
// the engine's workload.
_spellerInterop.SetContextOption("IsSpellChecking", false);
XmlLanguage language = (XmlLanguage)caretPosition.GetValue(FrameworkElement.LanguageProperty);
wordBreakLeft = SearchForWordBreaks(caretPosition, LogicalDirection.Backward, language, 1, false /* stopOnError */);
wordBreakRight = SearchForWordBreaks(caretPosition, LogicalDirection.Forward, language, 1, false /* stopOnError */);
textMap = new TextMap(wordBreakLeft, wordBreakRight, caretPosition, caretPosition);
segments = new ArrayList(2);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ExpandToWordBreakCallback), segments);
// We will have no segments when position is surrounded by
// nothing but white space.
if (segments.Count != 0)
{
int leftBreakOffset;
int rightBreakOffset;
// Figure out where caretPosition lives in the segment list.
FindPositionInSegmentList(textMap, LogicalDirection.Backward, segments, out leftBreakOffset, out rightBreakOffset);
wordBreakLeft = textMap.MapOffsetToPosition(leftBreakOffset);
wordBreakRight = textMap.MapOffsetToPosition(rightBreakOffset);
}
// Overlap?
if (wordBreakLeft.CompareTo(rawEnd) < 0 &&
wordBreakRight.CompareTo(rawStart) > 0)
{
if (wordBreakLeft.CompareTo(rawStart) > 0)
{
// Truncate the right half of the input range.
end = wordBreakLeft;
}
else if (wordBreakRight.CompareTo(rawEnd) < 0)
{
// Truncate the left half of the input range.
start = wordBreakRight;
}
else
{
// The entire dirty range is covered by the caret word.
// Try to find a following dirty range.
GetNextScanRangeRaw(wordBreakRight, out start, out end);
}
// Schedule a future callback to deal with the skipped
// overlapping section.
ScheduleCaretMovedCallback();
}
}
// Analyzes a run of text. The scan may be interrupted if we run out
// of time along the way, in which case some subset of the contained
// words will be left dirty.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private ScanStatus ScanRange(ITextPointer start, ITextPointer end, long timeLimit)
{
ITextPointer contextStart;
ITextPointer contextEnd;
ITextPointer contentStart;
ITextPointer contentEnd;
TextMap textMap;
ScanStatus status;
//
// IMPORTANT: the scan logic here (word break expansion, TextMap creation, etc.)
// must match GetSuggestionForError exactly. Keep the methods in [....]!
//
//
// Expand the content to include whole words.
// Also get pointers to sufficient surrounding text to analyze
// multi-word errors correctly.
//
status = new ScanStatus(timeLimit);
XmlLanguage language;
CultureInfo culture = GetCurrentCultureAndLanguage(start, out language);
if (culture == null)
{
// Someone set a bogus language on the run -- ignore it.
_statusTable.MarkCleanRange(start, end);
}
else
{
SetCulture(culture);
ExpandToWordBreakAndContext(start, LogicalDirection.Backward, language, out contentStart, out contextStart);
ExpandToWordBreakAndContext(end, LogicalDirection.Forward, language, out contentEnd, out contextEnd);
Invariant.Assert(contentStart.CompareTo(contentEnd) < 0);
Invariant.Assert(contextStart.CompareTo(contextEnd) < 0);
Invariant.Assert(contentStart.CompareTo(contextStart) >= 0);
Invariant.Assert(contentEnd.CompareTo(contextEnd) <= 0);
//
// Mark the range clean, before we scan for errors.
//
_statusTable.MarkCleanRange(contentStart, contentEnd);
//
// Read the text.
//
// Check for a compatible language.
if (CanSpellCheck(culture))
{
_spellerInterop.SetContextOption("IsSpellChecking", true);
_spellerInterop.SetContextOption("IsSpellVerifyOnly", true);
textMap = new TextMap(contextStart, contextEnd, contentStart, contentEnd);
//
// Iterate over sentences and segments.
//
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, new SpellerInterop.EnumSentencesCallback(ScanRangeCheckTimeLimitCallback),
new SpellerInterop.EnumTextSegmentsCallback(ScanTextSegment), new TextMapCallbackData(textMap, status));
if (status.TimeoutPosition != null)
{
if (status.TimeoutPosition.CompareTo(end) < 0)
{
// We ran out of time before analyzing the whole block.
// Reset the dirty status of the remainder.
_statusTable.MarkDirtyRange(status.TimeoutPosition, end);
// We should always make some forward progress, even just one word,
// otherwise we'll never finish checking the document.
if (status.TimeoutPosition.CompareTo(start) <= 0)
{
// Diagnostic info for bug 1577085.
string debugMessage = "Speller is not advancing! \n" +
"Culture = " + culture + "\n" +
"Start offset = " + start.Offset + " parent = " + start.ParentType.Name + "\n" +
"ContextStart offset = " + contextStart.Offset + " parent = " + contextStart.ParentType.Name + "\n" +
"ContentStart offset = " + contentStart.Offset + " parent = " + contentStart.ParentType.Name + "\n" +
"ContentEnd offset = " + contentEnd.Offset + " parent = " + contentEnd.ParentType.Name + "\n" +
"ContextEnd offset = " + contextEnd.Offset + " parent = " + contextEnd.ParentType.Name + "\n" +
"Timeout offset = " + status.TimeoutPosition.Offset + " parent = " + status.TimeoutPosition.ParentType.Name + "\n" +
"textMap TextLength = " + textMap.TextLength + " text = " + new string(textMap.Text) + "\n" +
"Document = " + start.TextContainer.Parent.GetType().Name + "\n";
if (start is TextPointer)
{
debugMessage += "Xml = " + new TextRange((TextPointer)start.TextContainer.Start, (TextPointer)start.TextContainer.End).Xml;
}
Invariant.Assert(false, debugMessage);
}
}
else
{
// We ran of time but finished the whole block.
// TimeoutPosition should never be past contentEnd.
// It might be less than contentEnd if the dirty run ends
// with an element edge, in which case TimeoutPosition
// will preceed the final element edge(s).
Invariant.Assert(status.TimeoutPosition.CompareTo(contentEnd) <= 0);
}
}
}
}
return status;
}
// Callback for the error segment scanned during error lookup.
// Returns a list of correction suggestions.
private bool ScanErrorTextSegment(object textSegment, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
SpellerInterop.STextRange sTextRange = SpellerInterop.GetSegmentRange(textSegment);
// Check if this segment falls outside the content range.
// The region before/after the content is only for context --
// to handle multi-word errors correctly. We never want to mark it.
if (sTextRange.Start + sTextRange.Length <= data.TextMap.ContentStartOffset)
{
// Preceeding context, skip this segment and keep going.
return true;
}
SpellerInterop.GetSuggestions(textSegment, (ArrayList)data.Data);
// We only expect one error segment for this callback, so skip any
// following context segments.
return false;
}
// Scans a single segment (the engine's formal notion of a "word").
// Called indirectly by ScanRange.
// Returns true to continue the segment enumeration, false to
// break out of the iteration.
private bool ScanTextSegment(object textSegment, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
SpellerInterop.STextRange sTextRange;
char[] word;
sTextRange = SpellerInterop.GetSegmentRange(textSegment);
// Check if this segment falls outside the content range.
// The region before/after the content is only for context --
// to handle multi-word errors correctly. We never want to mark it.
if (sTextRange.Start + sTextRange.Length <= data.TextMap.ContentStartOffset)
{
// Preceeding context, skip this segment and keep going.
return true;
}
if (sTextRange.Start >= data.TextMap.ContentEndOffset)
{
// Following context, skip this segment and stop iterating any remainder.
return false;
}
if (sTextRange.Length > 1) // Ignore single letter errors.
{
// Check if the segment has been marked "ignore" by the user.
word = new char[sTextRange.Length];
Array.Copy(data.TextMap.Text, sTextRange.Start, word, 0, sTextRange.Length);
if (!IsIgnoredWord(word))
{
SpellerInterop.RangeRole role = SpellerInterop.GetSegmentRole(textSegment);
if (role == SpellerInterop.RangeRole.ecrrIncorrect)
{
if (SpellerInterop.GetSegmentCount(textSegment) == 0)
{
// We have an error.
MarkErrorRange(data.TextMap, sTextRange);
}
else
{
// We have a subsegment with an error.
SpellerInterop.EnumSubsegments(textSegment,
new SpellerInterop.EnumTextSegmentsCallback(ScanTextSegment), data);
}
}
}
}
return true;
}
// Called after we've scanned all the segments within a sentence from inside ScanRange.
// This method returns false to the enumerator to end the scan if we've run out of time.
//
// NB: we terminate the segment enumeration run by ScanRange only at sentence boundaries,
// not at more finely grained segment boundaries. This is because the vast amount of
// work done takes place when preparing to iterate segments, so the incremental cost
// of actually walking the segments once calculated is ignorable, but halting the scan
// after looking at a single segment would mean repeating the overhead on all segments
// in the sentence.
private bool ScanRangeCheckTimeLimitCallback(object sentence, object o)
{
TextMapCallbackData data = (TextMapCallbackData)o;
ScanStatus status;
status = (ScanStatus)data.Data;
// Stop iterating if we exceed our time budget.
// In which case, take note of where we left off.
if (status.HasExceededTimeLimit)
{
Invariant.Assert(status.TimeoutPosition == null); // We should only set this once....
int sentenceEndOffset = SpellerInterop.GetSentenceEndOffset(sentence);
if (sentenceEndOffset >= 0)
{
// The end of this segment may extend past textMap.ContentEndOffset,
// in the case of multi-word errors. So truncate.
//
int timeOutOffset = Math.Min(data.TextMap.ContentEndOffset, sentenceEndOffset);
// Be careful not to stop the iteration if we haven't reached
// the content start yet. It's possible that the context text
// will extend backwards into another sentence. We must always
// make forward progress, even if doing so exceeds the timeout
// limit, otherwise we'll get stuck infinitely eating idle time.
//
//
if (timeOutOffset > data.TextMap.ContentStartOffset)
{
status.TimeoutPosition = data.TextMap.MapOffsetToPosition(timeOutOffset);
}
}
}
return (status.TimeoutPosition == null);
}
// Flags a run of text with an error.
// In two exceptional circumstances we schedule an idle-time callback
// to re-analyze the run instead of marking it:
// - when the caret is within the error text.
// - when an IME composition covers the text.
private void MarkErrorRange(TextMap textMap, SpellerInterop.STextRange sTextRange)
{
ITextPointer errorStart;
ITextPointer errorEnd;
if (sTextRange.Start + sTextRange.Length > textMap.ContentEndOffset)
{
// We found an error that starts in the content but extends into
// the context. This must be a multi-word error.
// For now, ignore it.
//
return;
}
errorStart = textMap.MapOffsetToPosition(sTextRange.Start);
errorEnd = textMap.MapOffsetToPosition(sTextRange.Start + sTextRange.Length);
if (sTextRange.Start < textMap.ContentStartOffset)
{
Invariant.Assert(sTextRange.Start + sTextRange.Length > textMap.ContentStartOffset);
// We've found an error that start in the context and extends into
// the content. This can happen as more text is revealed to the
// speller engine as the caret moves forward.
// E.g., while scanning "avalon's" we flag an error over "avalon",
// ignoring the "'s" because the caret is positioned within that segment.
// Then, the user hits space and now we analyze "'s" along with its
// preceding context "avalon". In this final scan, "avalon's" as a while
// is flagged as an error and we enter this if statement.
// We must mark the range clean before we can mark it dirty.
// _statusTable.MarkErrorRange can only handle clean runs.
_statusTable.MarkCleanRange(errorStart, errorEnd);
}
_statusTable.MarkErrorRange(errorStart, errorEnd);
}
// Examines a position and returns to two relative positions:
//
// contentPosition -> position moved inward to the nearest word
// break (opposite direction param, toward the content).
//
// contextPosition -> position moved outward away from content
// to a word break that includes sufficient text to handle multi-
// word errors correctly.
///
/// Critical - it calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a trusted parameter, that doesn't come from untrusted sources.
///
[SecurityCritical, SecurityTreatAsSafe]
private void ExpandToWordBreakAndContext(ITextPointer position, LogicalDirection direction, XmlLanguage language,
out ITextPointer contentPosition, out ITextPointer contextPosition)
{
ITextPointer start;
ITextPointer end;
ITextPointer outwardPosition;
ITextPointer inwardPosition;
TextMap textMap;
ArrayList segments;
SpellerInterop.STextRange sTextRange;
LogicalDirection inwardDirection;
int i;
contentPosition = position;
contextPosition = position;
if (position.GetPointerContext(direction) == TextPointerContext.None)
{
// There is no following context, we're at document start/end.
return;
}
// Disable spell checking functionality since we're only
// interested in word breaks here. This greatly cuts down
// the engine's workload.
_spellerInterop.SetContextOption("IsSpellChecking", false);
//
// Build an array of wordbreak offsets surrounding the position.
//
// 1. Search outward, into surrounding text. We need MinWordBreaksForContext
// word breaks to handle multi-word errors.
outwardPosition = SearchForWordBreaks(position, direction, language, MinWordBreaksForContext, true /* stopOnError */);
// 2. Search inward, towards content. We just need one word break inward.
inwardDirection = direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward;
inwardPosition = SearchForWordBreaks(position, inwardDirection, language, 1, false /* stopOnError */);
// Get combined word breaks. This may not be the same as we calculated
// in two parts above, since we don't know yet whether or not position is
// on a word break.
if (direction == LogicalDirection.Backward)
{
start = outwardPosition;
end = inwardPosition;
}
else
{
start = inwardPosition;
end = outwardPosition;
}
textMap = new TextMap(start, end, position, position);
segments = new ArrayList(MinWordBreaksForContext + 1);
_spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null,
new SpellerInterop.EnumTextSegmentsCallback(ExpandToWordBreakCallback), segments);
//
// Use our table of word breaks to calculate context and content positions.
//
if (segments.Count == 0)
{
// No segments. This can happen if position is surrounded by
// nothing but white space. We've already initialized contentPosition
// and contextPosition so there's nothing to do.
}
else
{
int leftWordBreak;
int rightWordBreak;
int contentOffset;
int contextOffset;
// Figure out where position lives in the segment list.
i = FindPositionInSegmentList(textMap, direction, segments, out leftWordBreak, out rightWordBreak);
// contentPosition should be an edge on the segment we found.
if (direction == LogicalDirection.Backward)
{
contentOffset = textMap.ContentStartOffset == rightWordBreak ? rightWordBreak : leftWordBreak;
}
else
{
contentOffset = textMap.ContentStartOffset == leftWordBreak ? leftWordBreak : rightWordBreak;
}
contentPosition = textMap.MapOffsetToPosition(contentOffset);
// contextPosition should be MinWordBreaksForContext - 1 words away.
if (direction == LogicalDirection.Backward)
{
i -= (MinWordBreaksForContext - 1);
sTextRange = (SpellerInterop.STextRange)segments[Math.Max(i, 0)];
// We might actually follow contentOffset if we're at the document edge.
// Don't let that happen.
contextOffset = Math.Min(sTextRange.Start, contentOffset);
}
else
{
i += MinWordBreaksForContext;
sTextRange = (SpellerInterop.STextRange)segments[Math.Min(i, segments.Count-1)];
// We might actually preceed contentOffset if we're at the document edge.
// Don't let that happen.
contextOffset = Math.Max(sTextRange.Start + sTextRange.Length, contentOffset);
}
contextPosition = textMap.MapOffsetToPosition(contextOffset);
}
// Final fixup: if the dirty range covers only formatting (which is not passed
// to the speller engine) then we might actually "expand" in the wrong
// direction, since the TextMap will jump over formatting.
// Backup if necessary.
if (direction == LogicalDirection.Backward)
{
if (position.CompareTo(contentPosition) < 0)
{
contentPosition = position;
}
if (position.CompareTo(contextPosition) < 0)
{
contextPosition = position;
}
}
else
{
if (position.CompareTo(contentPosition) > 0)
{
contentPosition = position;
}
if (position.CompareTo(contextPosition) > 0)
{
contextPosition = position;
}
}
}
// Helper for ExpandToWordBreakAndContext -- returns the index of a segment
// containing or bordering a specified position (textMap.ContentStartOffset).
// Also returns the offset, within the TextMap, of the two word breaks surrounding
// TextMap.ContentStartOffset. The word breaks may be segment edges, or
// the extent of a run of whitespace between two segments.
private int FindPositionInSegmentList(TextMap textMap, LogicalDirection direction, ArrayList segments,
out int leftWordBreak, out int rightWordBreak)
{
SpellerInterop.STextRange sTextRange;
int index;
// Make the compiler happy by initializing the out's to bogus values.
leftWordBreak = Int32.MaxValue;
rightWordBreak = -1;
// Check before the first segment, which start at the first
// non-whitespace char.
sTextRange = (SpellerInterop.STextRange)segments[0];
if (textMap.ContentStartOffset < sTextRange.Start)
{
leftWordBreak = 0;
rightWordBreak = sTextRange.Start;
index = -1;
}
else
{
// Check after the last segment, which does not include final whitespace.
sTextRange = (SpellerInterop.STextRange)segments[segments.Count-1];
if (textMap.ContentStartOffset > sTextRange.Start + sTextRange.Length)
{
leftWordBreak = sTextRange.Start + sTextRange.Length;
rightWordBreak = textMap.TextLength;
index = segments.Count;
}
else
{
// Walk the segment list, checking each segment and space in between.
for (index = 0; index < segments.Count; index++)
{
sTextRange = (SpellerInterop.STextRange)segments[index];
leftWordBreak = sTextRange.Start;
rightWordBreak = sTextRange.Start + sTextRange.Length;
// Check if we're inside this segment.
if (leftWordBreak <= textMap.ContentStartOffset &&
rightWordBreak >= textMap.ContentStartOffset)
{
break;
}
// Or if we're between this segment and the next one --
// segments do not include white space.
if (index < segments.Count - 1 &&
rightWordBreak < textMap.ContentStartOffset)
{
sTextRange = (SpellerInterop.STextRange)segments[index + 1];
leftWordBreak = rightWordBreak;
rightWordBreak = sTextRange.Start;
if (rightWordBreak > textMap.ContentStartOffset)
{
// position is between segments[i] and segments[i+1].
// Adjust i so that adding MinWordBreaksForContext below
// doesn't include an extra word.
if (direction == LogicalDirection.Backward)
{
index++;
}
break;
}
}
}
}
}
Invariant.Assert(leftWordBreak <= textMap.ContentStartOffset && textMap.ContentStartOffset <= rightWordBreak);
return index;
}
// Helper for ExpandToWordBreakAndContext -- returns the position
// of the nth word break in the specified direction.
// If stopOnError is true, the search will halt if an error run is
// encountered along the way. The search is also halted if text in
// a new language is encountered.
///
/// Critical - it calls EnumTextSegments(), which is Critical.
/// TreatAsSafe - it calls it passing a TextMap object constructed appropriately in this method.
///
[SecurityCritical, SecurityTreatAsSafe]
private ITextPointer SearchForWordBreaks(ITextPointer position, LogicalDirection direction, XmlLanguage language, int minWordCount, bool stopOnError)
{
ITextPointer closestErrorPosition;
ITextPointer searchPosition;
ITextPointer start;
ITextPointer end;
StaticTextPointer nextErrorTransition;
int segmentCount;
TextMap textMap;
searchPosition = position.CreatePointer();
closestErrorPosition = null;
if (stopOnError)
{
nextErrorTransition = _statusTable.GetNextErrorTransition(position.CreateStaticPointer(), direction);
if (!nextErrorTransition.IsNull)
{
closestErrorPosition = nextErrorTransition.CreateDynamicTextPointer(LogicalDirection.Forward);
}
}
bool hitBreakPoint = false;
do
{
searchPosition.MoveByOffset(direction == LogicalDirection.Backward ? -ContextBlockSize : +ContextBlockSize);
// Don't go past closestErrorPosition.
if (closestErrorPosition != null)
{
if (direction == LogicalDirection.Backward && closestErrorPosition.CompareTo(searchPosition) > 0 ||
direction == LogicalDirection.Forward && closestErrorPosition.CompareTo(searchPosition) < 0)
{
searchPosition.MoveToPosition(closestErrorPosition);
hitBreakPoint = true;
}
}
// Don't venture into text in another language.
ITextPointer closestLanguageTransition = GetNextLanguageTransition(position, direction, language, searchPosition);
if (direction == LogicalDirection.Backward && closestLanguageTransition.CompareTo(searchPosition) > 0 ||
direction == LogicalDirection.Forward && closestLanguageTransition.CompareTo(searchPosition) < 0)
{
searchPosition.MoveToPosition(closestLanguageTransition);
hitBreakPoint = true;
}
if (direction == LogicalDirection.Backward)
{
start = searchPosition;
end = position;
}
else
{
start = position;
end = searchPosition;
}
textMap = new TextMap(start, end, start, end);
segmentCount = _spellerInterop.EnumTextSegments(textMap.Text, textMap.TextLength, null, null, null);
}
while (!hitBreakPoint &&
segmentCount < minWordCount + 1 &&
searchPosition.GetPointerContext(direction) != TextPointerContext.None);
return searchPosition;
}
// Returns the closest of either a halting position or the position preceding text
// tagged with a differing XmlLanguage from a start position.
private ITextPointer GetNextLanguageTransition(ITextPointer position, LogicalDirection direction, XmlLanguage language, ITextPointer haltPosition)
{
ITextPointer navigator = position.CreatePointer();
while ((direction == LogicalDirection.Forward && navigator.CompareTo(haltPosition) < 0) ||
(direction == LogicalDirection.Backward && navigator.CompareTo(haltPosition) > 0))
{
if ((XmlLanguage)navigator.GetValue(FrameworkElement.LanguageProperty) != language)
break;
navigator.MoveToNextContextPosition(direction);
}
// If we moved past haltPosition on the final MoveToNextContextPosition, move back.
if ((direction == LogicalDirection.Forward && navigator.CompareTo(haltPosition) > 0) ||
(direction == LogicalDirection.Backward && navigator.CompareTo(haltPosition) < 0))
{
navigator.MoveToPosition(haltPosition);
}
return navigator;
}
// Called indirectly by ExpandToWordBreakAndContext while iterating segments.
// Builds up an array of segment offsets while iterating.
private bool ExpandToWordBreakCallback(object textSegment, object o)
{
ArrayList segments = (ArrayList)o;
SpellerInterop.STextRange sTextRange = SpellerInterop.GetSegmentRange(textSegment);
segments.Add(sTextRange);
return true;
}
// Returns true if a user has tagged the specified word with "Ignore All".
private bool IsIgnoredWord(char[] word)
{
bool isIgnoredWord = false;
if (_ignoredWordsList != null)
{
isIgnoredWord = _ignoredWordsList.BinarySearch(new string(word), new CaseInsensitiveComparer(_defaultCulture)) >= 0;
}
return isIgnoredWord;
}
// Returns true if we have an engine capable of proofing the specified
// language.
private static bool CanSpellCheck(CultureInfo culture)
{
bool canSpellCheck;
switch (culture.TwoLetterISOLanguageName)
{
case "en":
case "de":
case "fr":
case "es":
canSpellCheck = true;
break;
default:
canSpellCheck = false;
break;
}
return canSpellCheck;
}
// Sets the speller engine language and spelling reform options.
///
/// Critical - It calls SetContextOption(), which is Critical.
/// TreatAsSafe - it calls it with a well known option.
///
[SecurityCritical, SecurityTreatAsSafe]
private void SetCulture(CultureInfo culture)
{
//
// Set the language.
//
_spellerInterop.SetLocale(culture.LCID);
//
// Set spelling reform, if necessary.
//
string option;
// The engine only accepts values for german and french.
switch (culture.TwoLetterISOLanguageName)
{
case "de":
option = "GermanReform";
break;
case "fr":
option = "FrenchReform";
break;
default:
option = null;
break;
}
if (option != null)
{
switch (_spellingReform)
{
case SpellingReform.Prereform:
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Prereform);
break;
case SpellingReform.Postreform:
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Postreform);
break;
case SpellingReform.PreAndPostreform:
if (option == "GermanReform")
{
// BothPreAndPost is disallowed for german -- the engine has undefined results.
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.Postreform);
}
else
{
_spellerInterop.SetContextOption(option, SpellerInterop.SpellingReform.BothPreAndPost);
}
break;
}
}
}
// Scans the word containing a specified character.
private void ScanPosition(ITextPointer position, LogicalDirection direction)
{
ITextPointer start;
ITextPointer end;
if (direction == LogicalDirection.Forward)
{
start = position;
end = position.CreatePointer(+1);
}
else
{
start = position.CreatePointer(-1);
end = position;
}
XmlLanguage language = (XmlLanguage)position.GetValue(FrameworkElement.LanguageProperty);
ScanRange(start, end, Int64.MaxValue /* timeLimit */);
}
// Returns the CultureInfo of the content at a position.
// Returns null if there is no CultureInfo matching the current XmlLanguage.
private CultureInfo GetCurrentCultureAndLanguage(ITextPointer position, out XmlLanguage language)
{
CultureInfo cultureInfo;
bool hasModifiers;
// TextBox takes the input language iff no local LanguageProperty is set.
if (!_textEditor.AcceptsRichContent &&
_textEditor.UiScope.GetValueSource(FrameworkElement.LanguageProperty, null, out hasModifiers) == BaseValueSourceInternal.Default)
{
cultureInfo = _defaultCulture;
language = XmlLanguage.GetLanguage(cultureInfo.IetfLanguageTag);
}
else
{
language = (XmlLanguage)position.GetValue(FrameworkElement.LanguageProperty);
if (language == null)
{
cultureInfo = null;
}
else
{
try
{
cultureInfo = language.GetSpecificCulture();
}
catch (InvalidOperationException)
{
// Someone set a bogus language on the run.
cultureInfo = null;
}
}
}
return cultureInfo;
}
#endregion Private methods
//-----------------------------------------------------
//
// Private Types
//
//-----------------------------------------------------
#region Private Types
// Holds a run of text intended for the speller engine.
// Because the engine only understands plain text, this class coverts
// arbitrary runs of document text to speller-suitable plain text
// and keeps a table that allows it to efficiently map back from plain
// text offsets (used by the engine) to ITextPointers.
private class TextMap
{
// Creates a new instance.
// contextStart/End refer to the whole run of text.
// contentStart/End are a subset of the text, which is what
// the engine will actually tag with errors.
// The space between context and content is used by the engine
// to correctly analyze multiple word phrase like "Los Angeles"
// that could otherwise be truncated and incorrectly tagged.
internal TextMap(ITextPointer contextStart, ITextPointer contextEnd,
ITextPointer contentStart, ITextPointer contentEnd)
{
ITextPointer position;
int maxChars;
int inlineCount;
int runCount;
int i;
int distance;
Invariant.Assert(contextStart.CompareTo(contentStart) <= 0);
Invariant.Assert(contextEnd.CompareTo(contentEnd) >= 0);
_basePosition = contextStart.GetFrozenPointer(LogicalDirection.Backward);
position = contextStart.CreatePointer();
maxChars = contextStart.GetOffsetToPosition(contextEnd);
_text = new char[maxChars];
_positionMap = new int[maxChars+1];
_textLength = 0;
inlineCount = 0;
_contentStartOffset = 0;
_contentEndOffset = 0;
// Iterate over the run, building up a matching plain text buffer
// and a table that tells us how to map back to the original text.
while (position.CompareTo(contextEnd) < 0)
{
if (position.CompareTo(contentStart) == 0)
{
_contentStartOffset = _textLength;
}
if (position.CompareTo(contentEnd) == 0)
{
_contentEndOffset = _textLength;
}
switch (position.GetPointerContext(LogicalDirection.Forward))
{
case TextPointerContext.Text:
runCount = position.GetTextRunLength(LogicalDirection.Forward);
runCount = Math.Min(runCount, _text.Length - _textLength);
runCount = Math.Min(runCount, position.GetOffsetToPosition(contextEnd));
position.GetTextInRun(LogicalDirection.Forward, _text, _textLength, runCount);
for (i = _textLength; i < _textLength + runCount; i++)
{
_positionMap[i] = i + inlineCount;
}
distance = position.GetOffsetToPosition(contentStart);
if (distance >= 0 && distance <= runCount)
{
_contentStartOffset = _textLength + position.GetOffsetToPosition(contentStart);
}
distance = position.GetOffsetToPosition(contentEnd);
if (distance >= 0 && distance <= runCount)
{
_contentEndOffset = _textLength + position.GetOffsetToPosition(contentEnd);
}
position.MoveByOffset(runCount);
_textLength += runCount;
break;
case TextPointerContext.ElementStart:
case TextPointerContext.ElementEnd:
if (IsAdjacentToFormatElement(position))
{
// Filter out formatting tags from the plain text.
inlineCount++;
}
else
{
// Stick in a word break to account for the block element.
_text[_textLength] = ' ';
_positionMap[_textLength] = _textLength + inlineCount;
_textLength++;
}
position.MoveToNextContextPosition(LogicalDirection.Forward);
break;
case TextPointerContext.EmbeddedElement:
_text[_textLength] = '\xf8ff'; // Unicode private use.
_positionMap[_textLength] = _textLength + inlineCount;
_textLength++;
position.MoveToNextContextPosition(LogicalDirection.Forward);
break;
}
}
if (position.CompareTo(contentEnd) == 0)
{
_contentEndOffset = _textLength;
}
if (_textLength > 0)
{
_positionMap[_textLength] = _positionMap[_textLength - 1] + 1;
}
else
{
_positionMap[0] = 0;
}
Invariant.Assert(_contentStartOffset <= _contentEndOffset);
}
// Returns an ITextPointer in the document with position matching
// an offset within the plain text.
internal ITextPointer MapOffsetToPosition(int offset)
{
Invariant.Assert(offset >= 0 && offset <= _textLength);
return _basePosition.CreatePointer(_positionMap[offset]);
}
// Offset in the plain text of the content start.
internal int ContentStartOffset
{
get { return _contentStartOffset; }
}
// Offset in the plain text of the content end.
internal int ContentEndOffset
{
get { return _contentEndOffset; }
}
// Plain text representation of the document run.
// Do not use Text.Length! The actual content size may be smaller,
// use the TextLength property instead.
internal char[] Text
{
get { return _text; }
}
// Length of the plain text. This may be less than Text.Length --
// we allocate a maximum value for the array which is not always used
// by the final content.
internal int TextLength
{
get { return _textLength; }
}
// Returns true if pointer preceeds an Inline start or end edge.
private bool IsAdjacentToFormatElement(ITextPointer pointer)
{
TextPointerContext context;
bool isAdjacentToFormatElement;
isAdjacentToFormatElement = false;
context = pointer.GetPointerContext(LogicalDirection.Forward);
if (context == TextPointerContext.ElementStart &&
TextSchema.IsFormattingType(pointer.GetElementType(LogicalDirection.Forward)))
{
isAdjacentToFormatElement = true;
}
else if (context == TextPointerContext.ElementEnd &&
TextSchema.IsFormattingType(pointer.ParentType))
{
isAdjacentToFormatElement = true;
}
return isAdjacentToFormatElement;
}
// Position of the plain text block within the document.
private readonly ITextPointer _basePosition;
// Plain text version of the document run.
private readonly char[] _text;
// Map of plain text offsets to document symbol offsets relative
// to _basePosition.
private readonly int[] _positionMap;
// Size of the content within _text.
private readonly int _textLength;
// Plain text offset of the content start.
private readonly int _contentStartOffset;
// Plain text offset of the content end.
private readonly int _contentEndOffset;
}
// Holds state tracking the progress of speller scan,
// used during idle time document analysis.
private class ScanStatus
{
// Creates a new instance. timeLimit is the maximum value of
// DateTime.Now.Ticks at which the scan should end.
internal ScanStatus(long timeLimit)
{
_timeLimit = timeLimit;
}
// Returns true if the scan has exceeded its time budget.
internal bool HasExceededTimeLimit
{
get
{
long nowTicks = DateTime.Now.Ticks;
#if DEBUG
// Track how far over budget we are the first time we check.
if (nowTicks >= _timeLimit && _debugMsOverTimeLimit == 0)
{
_debugMsOverTimeLimit = (int)(((double)(nowTicks - _timeLimit)) / 10000);
}
#endif // DEBUG
return nowTicks >= _timeLimit;
}
}
// If we've timed out, holds the position we left off -- the remainder
// of the text run yet to be analyzed.
internal ITextPointer TimeoutPosition
{
get { return _timeoutPosition; }
set { _timeoutPosition = value; }
}
// Budget for this scan, in 100 nanosecond intervals.
private readonly long _timeLimit;
// If we've timed out, holds the position we left off -- the remainder
// of the text run yet to be analyzed.
private ITextPointer _timeoutPosition;
#if DEBUG
// Number of milliseconds we've exceeded our time limit by.
private int _debugMsOverTimeLimit;
#endif // DEBUG
}
// Container used to hold state for SpellerInterop callbacks.
private class TextMapCallbackData
{
internal TextMapCallbackData(TextMap textmap, object data)
{
_textmap = textmap;
_data = data;
}
internal TextMap TextMap { get { return _textmap; } }
internal object Data { get { return _data; } }
private readonly TextMap _textmap;
private readonly object _data;
}
#endregion Private Types
//-----------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
// Max time slice for speller background proofing, in milliseconds.
// Larger numbers mean we can scan entire documents more quickly,
// but with less responsiveness to user interruptions.
private const int MaxIdleTimeSliceMs = 20;
// Max time slice for speller background proofing, in 100 nanosecond intervals.
private const long MaxIdleTimeSliceNs = MaxIdleTimeSliceMs*10000;
// Max number of characters to pass to engine in a single call.
// Increasing this number will decrease the time to scan an entire
// document, at the cost of more time spent in individual Idle callbacks
// (app is less respsonsive).
// NLG devs have warned us not to set this value below 32, to avoid
// cases where they don't have enough context to identify errors
// correctly.
private const int MaxScanBlockSize = 64;
// Number of characters to advance on each iteration while searching
// for context. We need at least three words on either side of a text
// run to give the speller enough context to identify multi-word
// errors (or non-errors, like Los Angeles).
//
// Larger numbers here will mean fewer text scans, but a smaller
// minimum scan.
private const int ContextBlockSize = 32;
// The minimum number of word breaks we need to have sufficient
// context for detecting multi-word errors. This number is
// a constant -- it is not something to adjust for perf.
private const int MinWordBreaksForContext = 4;
// TextEditor that owns this Speller.
private TextEditor _textEditor;
// A run-length array tracking speller status of all text in the document.
private SpellerStatusTable _statusTable;
// HighlightLayer used to display error squiggles.
private SpellerHighlightLayer _highlightLayer;
// Engine object used to analyze runs of text.
// kepowell from the nlg team suggests that we cache a single
// ITextChunk/ITextContext for the thread and reuse it across
// Spellers. FE TextChunks in particular are expensive, because
// they cache large amounts of data per instance, on the order
// of 10k's of data.
///
/// Critical: This object could expose a COM object which can run code under elevation
///
[SecurityCritical]
private SpellerInterop _spellerInterop;
// Current spelling reform setting.
private SpellingReform _spellingReform;
// true if we've already posted but not yet received a background queue item.
private bool _pendingIdleCallback;
// true if we have an active TextSelection.Changed listener.
private bool _pendingCaretMovedCallback;
// List of words tagged by the user as non-errors.
private ArrayList _ignoredWordsList;
// The CultureInfo associated with this speller.
// Used for ignored words comparison, and plain text controls (TextBox).
private readonly CultureInfo _defaultCulture;
// Set true if the nl6 library is unavailable.
private bool _failedToInit;
#endregion Private Fields
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- ParseChildrenAsPropertiesAttribute.cs
- CompiledRegexRunnerFactory.cs
- Brush.cs
- CodeAttributeArgument.cs
- ObjectDataSource.cs
- DeadCharTextComposition.cs
- TaskScheduler.cs
- WorkerRequest.cs
- ValidationErrorCollection.cs
- ToolboxService.cs
- ToolStripArrowRenderEventArgs.cs
- ParentControlDesigner.cs
- AutoResetEvent.cs
- HttpWebRequest.cs
- EnumDataContract.cs
- Processor.cs
- Ref.cs
- DataGridItem.cs
- filewebrequest.cs
- HandlerFactoryCache.cs
- ConstructorBuilder.cs
- MSG.cs
- EntityCommand.cs
- Pts.cs
- Lazy.cs
- SqlRewriteScalarSubqueries.cs
- DbMetaDataColumnNames.cs
- SessionStateContainer.cs
- RootBrowserWindow.cs
- AmbientEnvironment.cs
- SqlClientPermission.cs
- Ref.cs
- MailAddress.cs
- GridProviderWrapper.cs
- ObjectSpanRewriter.cs
- AsyncStreamReader.cs
- ColorTransformHelper.cs
- DiscoveryMessageSequenceGenerator.cs
- Interlocked.cs
- ClientScriptManager.cs
- QueryRewriter.cs
- IndexerReference.cs
- TypeSystemProvider.cs
- CategoriesDocument.cs
- ResourceContainer.cs
- DictionarySectionHandler.cs
- DataSourceGeneratorException.cs
- ColumnPropertiesGroup.cs
- SQLGuidStorage.cs
- ObjectManager.cs
- FlagsAttribute.cs
- UserNameSecurityTokenProvider.cs
- QuadraticBezierSegment.cs
- PerformanceCounterPermissionEntry.cs
- XmlNodeReader.cs
- IntegrationExceptionEventArgs.cs
- FixedSOMTextRun.cs
- NetWebProxyFinder.cs
- ObjectConverter.cs
- OdbcInfoMessageEvent.cs
- HuffmanTree.cs
- CorrelationToken.cs
- SQLGuid.cs
- ReadContentAsBinaryHelper.cs
- TypeUsage.cs
- PassportIdentity.cs
- unitconverter.cs
- RelatedCurrencyManager.cs
- ThemeDirectoryCompiler.cs
- TcpServerChannel.cs
- SpecularMaterial.cs
- BooleanToVisibilityConverter.cs
- FixedTextView.cs
- Double.cs
- FormCollection.cs
- CommandArguments.cs
- XsdDateTime.cs
- ConstructorExpr.cs
- ECDiffieHellmanPublicKey.cs
- EventLogException.cs
- counter.cs
- JsonByteArrayDataContract.cs
- TextureBrush.cs
- Common.cs
- FixedSchema.cs
- SQLInt64Storage.cs
- Evidence.cs
- ReadOnlyHierarchicalDataSource.cs
- SqlCacheDependencySection.cs
- EventMemberCodeDomSerializer.cs
- SerializableAttribute.cs
- BamlTreeUpdater.cs
- ResourceDisplayNameAttribute.cs
- QuadraticBezierSegment.cs
- TagPrefixInfo.cs
- StructuredType.cs
- HTTPRemotingHandler.cs
- WebServiceClientProxyGenerator.cs
- AnnotationResource.cs
- Validator.cs