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; }
internal TextMapCallbackData(TextMap textmap, object data) { _textmap = textmap; _data = data; }
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 = GetCurrentLanguage(caretPosition); 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(); } }
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; }
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; }
// 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; }
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; } } }
// 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); }