自定义 NSTextView insertText:replacementRange 中断拼写检查

Custom NSTextView insertText:replacementRange breaks Spell Checking

我有一个自定义的 NSTextView 子类,还有一个自定义的 NSTextStorage 组件。 NSTextStorage 根据上下文修改用户输入的文本。

因为最终文本可能比用户最初输入的文本短,所以我不得不在我的 NSTextView 中覆盖 insertText:replacementRange。一个最小的例子是:

- (void) insertText:(id)string replacementRange:(NSRange)replacementRange {
    if ([self hasMarkedText]) {
        [[self textStorage] replaceCharactersInRange:[self markedRange] withString:string];
    } else {
        [[self textStorage] replaceCharactersInRange:[self selectedRange] withString:string];
    }

    [self didChangeText];
}

这在几个月的广泛测试中工作正常....除了禁用自动拼写检查和更正。 "squigglies" 不会出现在拼写错误的单词下方,除非我停止键入、移动鼠标​​并将焦点切换到我的应用程序或从我的应用程序切换焦点。几秒钟后,整个文本视图都进行了拼写检查。因为是事后发生的,所以自动更正当然是禁用的。

如果我禁用我的自定义 insertText:replacementRange: 方法,其他一切正常,自动拼写功能 returns。我只需要注意不要触发导致缩短文本的更改,因为它会触发属性超出范围错误(首先是我的自定义方法的原始原因。)

显然 Apple 的 insertText:replacementRange: 实现比我的要好得多。我已经尝试了 [self checkTextInRange...][self checkTextInSelection:] 等的多种变体。None 恢复了正常的功能。

搜索 Apple 的文档并不能帮助我找到我从我的方法中遗漏的导致拼写检查中断的内容。任何指示或想法将不胜感激!!

提前致谢!

编辑:以下是我的 NSTextStorage 提供的各种行为的一些示例。 (|代表插入符号)

开始于:

* item
* |

如果我按下 return 键,我将得到以下结果(删除 *<space>):

* item
|

另一个例子,如果启用"Change Tracking":

this is thee| time

如果我点击删除:

this is the|{--e--} time

如您所见,一次击键可能会导致在文本中添加或删除多个字符。

编辑 2:仅供参考 - 当在文档末尾按 return 时发生缩短时,我遇到的属性超出范围的问题 - - NSTextview 尝试设置新的段落样式,结果发现文档比预期的要短。我找不到更改 NSTextview 目标范围的方法。

我有部分解决方案。

在我的自定义 insertText:replacementRange: 方法中,在 didChangeText 之前:

NSinteger wordCount;
NSOrthography * orthography;

static NSInteger theWordCount;
NSOrthography  * orthography;

NSRange spellingRange = <range to check>

NSArray * results = [[NSSpellChecker sharedSpellChecker] checkString:[[self textStorage] string]
                                                               range:spellingRange
                                                               types:[self enabledTextCheckingTypes]
                                                             options:NULL
                                              inSpellDocumentWithTag:0
                                                         orthography:&orthography
                                                           wordCount:&theWordCount];
if (results.count) {
    [self handleTextCheckingResults:results forRange:spellingRange types:[self enabledTextCheckingTypes] options:@{} orthography:orthography wordCount:theWordCount];
}

然而,这是不完整的:

  • 拼写检查和语法检查工作正常
  • 自动拼写更正和文本替换不起作用(即使启用)

(2018-05-30 编辑)

更新回复 (2018-05-22):

这个问题再次浮出水面,我真的需要弄清楚。

  1. 我的自定义 NSTextStorage 与描述的基本相同,并且仍然有效。

  2. 我在我的 NSTextView 上使用自定义 insertText:replacementRange:,但它调用 [super insertText:replacementRange:] 以利用 Apple 的幕后工作,使拼写等工作更好.我的自定义方法只需要设置一个布尔值。

  3. 当缩短文本时,我仍然收到 Apple insertText:replacementRange: 对文本不存在部分的属性的请求。以前,我会卡在这里,因为我尝试的一切要么导致崩溃,要么导致 Apple 的代码无限期地重复请求不存在的属性。

  4. 最后,我尝试 return 使用 NULL 范围指针伪造属性,这似乎让 Apple 的代码满意:

    - (NSDictionary *) attributesAtIndex:(NSUInteger)location effectiveRange:(nullable NSRangePointer)range {
        if (location > _backingAttributes.length) {
            // This happens if we shrink the text before the textview is aware of it.
            // For example, if we expand "longtext" -> "short" in our smart string, then
            // The textview may set and request attributes past the end of our
            // _backing string.
            // Initially this was due to error in my code, but now I had to add
            // This error checking back
            NSLog(@"get attributes at (%lu) in (%lu)", (unsigned long)location, (unsigned long)_backingAttributes.length);
            NSLog(@"error");
    
            // Apparently returning fake attributes satisfies [NSTextView insertText:replacementRange:]
            range = NULL;
            return  @{
                      NSForegroundColorAttributeName : [BIColor redColor],
                      NSFontAttributeName : [BIFont fontWithName:@"Helvetica" size:14.0]
                      };
    
        } else {
            return [_backingAttributes attributesAtIndex:location effectiveRange:range];
        }
    }
    
  5. 经过进一步测试,事实证明这还不够。我最终将以下内容添加到 setter 以存储 macOS 试图设置的无效属性和范围:

    - (void) setAttributes:(NSDictionary<NSString *,id> *)attrs range:(NSRange)range {
        if (NSMaxRange(range) > _backingAttributes.length) {
            _invalidAttrs = attrs;
            _invalidRange = range;
        } else {
            [self beginEditing];
            [_backingAttributes setAttributes:attrs range:range];
            [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
            [self endEditing];
        }
    }
    
  6. 我将 `attributesAtIndex:effectiveRange: 更新为 return 调用无效范围时的以下内容,而不是 return 上面的假属性:

    // Apparently returning fake attributes satisfies [NSTextView insertText]
    *range = _invalidRange;
    return _invalidAttrs;
    

这似乎在以前会触发异常或无限循环的各种条件下都有效。