在编辑器中更改文本

编辑是对 Visual Studio 编辑器中打开的文本文档的更改。 Visual Studio 中的用户交互或语言服务和其他扩展的编程更改可能会导致编辑。 扩展必须准备好处理实时发生的文档文本更改。

在主 Visual Studio 集成开发环境(IDE)进程外部运行的扩展使用异步设计模式与 Visual Studio IDE 进程进行通信。 此行为意味着使用异步方法调用,这体现在 C# 中的 async 关键字以及方法名称中的 Async 后缀上。 异步性是编辑器在使用者期望其响应操作时的显著优势。

如果传统的同步 API 调用花费的时间超过预期,它将停止响应用户输入。 这种情况会创建一个 UI 冻结,该冻结持续到 API 调用完成。 新式交互式应用程序的用户希望文本编辑器始终保持响应状态,并且永远不会阻止它们正常工作。 为了满足这些用户的期望,必须具有异步扩展。

有关异步编程的详细信息,请参阅使用 Async 和 Await 的异步编程

在新的 Visual Studio 扩展性模型中,扩展是相对于用户的第二类。 它不能直接修改编辑器或文本文档。 所有状态更改都是异步和协作的。 Visual Studio IDE 执行扩展请求的更改。 该扩展可以请求对文档或文本视图的特定版本进行一个或多个更改。 例如,如果文档的该区域已更改,则可能会拒绝来自扩展的更改。

使用 EditAsync() 方法在 EditorExtensibility 上请求编辑。

如果熟悉旧版 Visual Studio 扩展,ITextDocumentEditor 与从 ITextBufferITextDocument 中更改状态的方法几乎相同,并支持大多数相同的功能。

MutationResult result = await this.Extensibility.Editor().EditAsync(
batch =>
{
    var editor = document.AsEditable(batch);
    editor.Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);

为了避免编辑错误,编辑器扩展的编辑按以下方式应用:

  1. 扩展请求根据文档的最新版本进行编辑。
  2. 该请求可能包含一个或多个文本修改、光标位置更改等。 可以更改单个EditAsync()请求中实现IEditable的任何类型,包括ITextViewSnapshotITextDocumentSnapshot。 编辑器执行编辑。 可以通过AsEditable()请求对特定类进行编辑。
  3. 编辑请求将发送到 Visual Studio IDE。 只有当请求的版本之后对象没有改变时,操作才会成功。 如果文档确实发生更改,可能会拒绝更改。 然后,该扩展必须在较新版本上重试。 突变运算的结果存储在 result其中。
  4. 编辑以原子方式应用,这意味着其他执行线程不会中断。 最佳做法是在单个 EditAsync() 调用中完成短时间内需要进行的所有更改。 这种做法可减少因用户编辑或语言服务操作在编辑之间发生而导致的意外行为的可能性。 例如,由于 Roslyn C# 移动了插入符号,扩展编辑被交错。

更改插入点位置或从扩展中选择文本

从扩展中编辑文本文档会隐式影响插入点位置。 例如,在光标处插入一些文本后,光标会移动到所插入文本的末尾。 扩展还可以使用 ITextViewSnapshot.AsEditable().SetSelections() 显式将插入点设置到不同的位置,或进行文本选择。 为了说明,以下代码会插入一些文本,但光标保持在原始位置。

await this.Extensibility.Editor().EditAsync(batch =>
{
    var caret = textView.Selection.Extent.Start;
    textView.Document.AsEditable(batch).Replace(textView.Selection.Extent, newGuidString);
    textView.AsEditable(batch).SetSelections([new Selection(activePosition: caret, anchorPosition: caret, insertionPosition: caret)]);
},
cancellationToken);

并发执行

⚠️ 编辑器扩展有时可以并发运行。

初始版本有一个已知问题,可能会导致编辑器扩展代码并发执行。 每个异步方法都按正确的顺序调用,但在第一个 await 方法之后,后续执行可能会发生交错。 如果扩展依赖于执行顺序,请考虑维护传入请求队列以保留订单,直到此问题得到解决。

有关详细信息,请参阅 StreamJsonRpc 默认排序和并发