Muistiinpano
Tämän sivun käyttö edellyttää valtuutusta. Voit yrittää kirjautua sisään tai vaihtaa hakemistoa.
Tämän sivun käyttö edellyttää valtuutusta. Voit yrittää vaihtaa hakemistoa.
Use the VisualStudio.Extensibility editor APIs to read document content, make text edits, handle selections, and respond to editor events. All edits are asynchronous, atomic, and cooperative — the Visual Studio IDE applies your requested changes on your behalf.
Note
This guide uses the VisualStudio.Extensibility SDK. See Choose the right extensibility model for model comparison.
Related samples
The samples repo includes text-editing samples such as InsertGuid ↗ and the DocumentEditing feature gallery, which demonstrate GetActiveTextViewAsync and EditAsync. Additional editor-focused samples demonstrate reacting to text view lifecycle events and filtering which documents an editor part applies to:
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace MarkdownLinter;
using System.Threading;
using System.Threading.Tasks;
using Microsoft;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
/// <summary>
/// Listener for text view lifetime events to start linter on new documents or changed documents.
/// </summary>
[VisualStudioContribution]
internal class TextViewEventListener : ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
#pragma warning disable CA2213 // This is an extension scoped service.
private readonly MarkdownDiagnosticsService diagnosticsProvider;
#pragma warning restore CA2213
/// <summary>
/// Initializes a new instance of the <see cref="TextViewEventListener"/> class.
/// </summary>
/// <param name="extension">Extension instance.</param>
/// <param name="extensibility">Extensibility object.</param>
/// <param name="diagnosticsProvider">Local diagnostics provider service instance.</param>
public TextViewEventListener(MarkdownLinterExtension extension, VisualStudioExtensibility extensibility, MarkdownDiagnosticsService diagnosticsProvider)
: base(extension, extensibility)
{
this.diagnosticsProvider = Requires.NotNull(diagnosticsProvider, nameof(diagnosticsProvider));
}
/// <inheritdoc/>
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromGlobPattern("**/*.md", true)],
};
/// <inheritdoc />
public Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken cancellationToken)
{
return this.diagnosticsProvider.ProcessTextViewAsync(args.AfterTextView, cancellationToken);
}
/// <inheritdoc />
public async Task TextViewClosedAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
await this.diagnosticsProvider.ClearEntriesForDocumentAsync(textViewSnapshot.Document.Uri, cancellationToken);
}
/// <inheritdoc />
public Task TextViewOpenedAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
return this.diagnosticsProvider.ProcessTextViewAsync(textViewSnapshot, cancellationToken);
}
}
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace DocumentSelectorSample;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Documents;
using Microsoft.VisualStudio.Extensibility.Editor;
/// <summary>
/// A sample demonstrating how to define an editor extension that is only applicable to files matching a file path pattern.
/// This text view event listener monitors users opening/modifying/closing C# files in "tests" folder and emulates running
/// unit tests in these files.
/// </summary>
[VisualStudioContribution]
internal class UnitTestRunner : ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
private readonly TextViewChangesAggregator textViewChangesAggregator;
private OutputChannel? outputChannel;
/// <summary>
/// Initializes a new instance of the <see cref="UnitTestRunner"/> class.
/// </summary>
/// <param name="extension">Extension instance.</param>
/// <param name="extensibility">Extensibility object.</param>
public UnitTestRunner(Extension extension, VisualStudioExtensibility extensibility)
: base(extension, extensibility)
{
this.textViewChangesAggregator = new((beforeTextView, afterTextView, cancellationToken) => this.RunUnitTestsAfterDelayAsync(afterTextView, cancellationToken));
}
/// <inheritdoc />
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromGlobPattern("**/tests/*.cs", relativePath: false)],
};
/// <inheritdoc />
public Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken cancellationToken)
{
return this.textViewChangesAggregator.TextViewChangedAsync(args, cancellationToken);
}
/// <inheritdoc />
public Task TextViewOpenedAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
return this.RunUnitTestsAfterDelayAsync(textViewSnapshot, cancellationToken);
}
/// <inheritdoc />
public Task TextViewClosedAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
return this.StopUnitTestsAsync(textViewSnapshot, cancellationToken);
}
/// <inheritdoc />
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
this.outputChannel?.Dispose();
this.textViewChangesAggregator.Dispose();
}
}
private async Task RunUnitTestsAfterDelayAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
await Task.Delay(500, cancellationToken);
await this.WriteToOutputWindowAsync($"Running unit tests in {textViewSnapshot.Document.Uri.LocalPath}", cancellationToken);
}
private async Task StopUnitTestsAsync(ITextViewSnapshot textViewSnapshot, CancellationToken cancellationToken)
{
await this.WriteToOutputWindowAsync($"Stop running unit tests in {textViewSnapshot.Document.Uri.LocalPath}", cancellationToken);
}
private async Task WriteToOutputWindowAsync(string message, CancellationToken cancellationToken)
{
this.outputChannel ??= await this.Extensibility.Views().Output.CreateOutputChannelAsync(
Resources.OutputWindowPaneName,
cancellationToken);
await this.outputChannel.WriteLineAsync(message);
}
}
Tip
See the related samples: MarkdownLinter ↗, DocumentSelectorSample ↗, and InsertGuid ↗ (demonstrates EditAsync text modifications).
Related concepts: Editor concepts — text view lifecycle, document types, and editor architecture, Editor RPC — Remote Procedure Call communication between extension and editor
Understand the example
Get the active text view
From a command handler, get the currently active text view and its document:
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken ct)
{
using var textView = await context.GetActiveTextViewAsync(ct);
if (textView is null) return; // No text editor is active
ITextDocumentSnapshot document = textView.Document;
string allText = document.Text.CopyToString(); // Full document content
Uri uri = document.Uri; // Document URI
}
Read text
Read the full document
ITextDocumentSnapshot document = textView.Document;
string allText = document.Text.CopyToString();
Read a specific line
ITextDocumentSnapshot document = textView.Document;
ITextDocumentSnapshotLine line = document.Lines[0]; // First line (zero-indexed)
string lineText = line.Text.CopyToString();
Read the current selection
foreach (var selection in textView.Selections)
{
if (!selection.IsEmpty)
{
var span = selection.Extent;
// Use span.Start and span.Length to read selected text
}
}
Edit text
All document modifications go through EditAsync(). This method batches one or more edits into a single atomic operation and creates an implicit undo transaction.
Insert text at a position
int position = textView.Selection.Extent.Start;
await this.Extensibility.Editor().EditAsync(
batch =>
{
ITextDocumentSnapshot document = textView.Document;
var doc = document.AsEditable(batch);
doc.Insert(position, "inserted text");
},
ct);
Replace text in selections
await this.Extensibility.Editor().EditAsync(
batch =>
{
var doc = textView.Document.AsEditable(batch);
foreach (var selection in textView.Selections)
{
if (!selection.IsEmpty)
{
doc.Replace(selection.Extent, newText);
}
}
},
ct);
Insert text and control caret position
By default, inserting text moves the caret to the end of the insertion. To keep the caret at its original position:
await this.Extensibility.Editor().EditAsync(
batch =>
{
var caret = textView.Selection.Extent.Start;
textView.Document.AsEditable(batch).Replace(textView.Selection.Extent, newText);
textView.AsEditable(batch).SetSelections(
[new Selection(activePosition: caret, anchorPosition: caret, insertionPosition: caret)]);
},
ct);
How edits are applied
- The extension requests edits based on its most recent document snapshot.
- The request can contain multiple text edits, caret changes, or selection changes. Any type that implements
IEditablecan be changed in a singleEditAsync()call. - The IDE applies the edits only if the document hasn't changed since the snapshot. If it has, the edit is rejected — retry on a newer snapshot.
- Edits are applied atomically with no interruptions from other threads. Batch all related changes into a single
EditAsync()call to avoid interleaving.
Filter by document type
Use TextViewExtensionConfiguration with DocumentFilter to target specific file types.
Filter by glob pattern
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromGlobPattern("**/*.cs", relativePath: false)],
};
Filter by document type
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromDocumentType("CSharp")],
};
Common document types: CSharp, C/C++, TypeScript, HTML, JSON, text, code.
Document types are hierarchical — code activates for all code languages (C#, C, C++, and so on).
Define a custom document type
internal static class MyDocumentTypes
{
[VisualStudioContribution]
internal static DocumentTypeConfiguration MarkdownDocumentType => new("markdown")
{
FileExtensions = [".md", ".mdk", ".markdown"],
BaseDocumentType = DocumentType.KnownValues.Text,
};
}
Glob pattern syntax
| Pattern | Meaning |
|---|---|
* |
Match zero or more characters in a path segment |
? |
Match one character in a path segment |
** |
Match any number of path segments, including none |
{} |
Group conditions (for example, **/*.{ts,js}) |
[] |
Character range (for example, example.[0-9]) |
Note
Backslashes aren't valid in glob patterns. Convert backslashes to forward slashes (/).
Listen to editor events
Respond to file open and close
Implement ITextViewOpenClosedListener to run code when files are opened or closed:
[VisualStudioContribution]
internal class MyTextViewListener : ExtensionPart, ITextViewOpenClosedListener
{
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromGlobPattern("**/*.md", relativePath: false)],
};
public async Task TextViewOpenedAsync(ITextViewSnapshot textView, CancellationToken ct)
{
// React to file open
}
public async Task TextViewClosedAsync(ITextViewSnapshot textView, CancellationToken ct)
{
// React to file close
}
}
Respond to text changes
Implement ITextViewChangedListener to react when the user or another extension modifies the document:
[VisualStudioContribution]
internal class MyTextChangeListener : ExtensionPart, ITextViewChangedListener
{
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromDocumentType("CSharp")],
};
public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken ct)
{
// Inspect args for what changed
}
}
You can implement both ITextViewOpenClosedListener and ITextViewChangedListener on the same class. The TextViewExtensionConfiguration applies to both.
Tips
✅ DO
- Use
EditAsync()for all document modifications — edits are batched and atomic. - Check
selection.IsEmptybefore processing selections. - Use
DocumentFilter.FromGlobPattern()to target specific file types. - Handle multiple selections — Visual Studio supports multi-cursor editing.
- Pass
CancellationTokenon all async calls.
❌ DON'T
- Don't modify documents outside of
EditAsync()— changes won't persist. - Don't assume a single selection — always iterate
textView.Selections. - Don't hold references to
ITextDocumentSnapshotlong-term — it's an immutable snapshot, not a live reference. - Don't block the editor thread with synchronous work.
🔧 Troubleshooting
| Symptom | Fix |
|---|---|
| Edits don't appear | Ensure you're using EditAsync() and calling methods on AsEditable(batch). |
| Extension only works on certain files | Check AppliesTo glob pattern; use relativePath: false for absolute paths. |
| Multiple selections ignored | Loop over textView.Selections instead of using a single selection. |
| Edit rejected silently | EditAsync() returns an EditResponse with Succeeded and Message properties. Check EditResponse.Succeeded — the document may have changed since your snapshot. Get a fresh snapshot and retry. |
Next steps
- Editor margins — add visual elements beside the editor
- CodeLens — add inline information above code lines
- Taggers — mark up text with visual indicators
- Editor concepts — deep dive on the editor object model