Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"EncodeDecodeBase64.EncodeDecodeBase64Command.DisplayName": "Encode / Decode Base64"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<NeutralLanguage>en-US</NeutralLanguage>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Sdk" Version="17.13.40008" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.13.40008" PrivateAssets="all" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace EncodeDecodeBase64;

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
using Microsoft.VisualStudio.Extensibility.Editor;

/// <summary>
/// Command handler to encode or decode a base64 text from the currently selected text.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="EncodeDecodeBase64Command"/> class.
/// </remarks>
/// <param name="traceSource">Trace source instance to utilize.</param>
[VisualStudioContribution]
internal class EncodeDecodeBase64Command(TraceSource traceSource) : Command
{
private readonly TraceSource logger = traceSource;

/// <inheritdoc />
public override CommandConfiguration CommandConfiguration => new("%EncodeDecodeBase64.EncodeDecodeBase64Command.DisplayName%")
{
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
Icon = new(ImageMoniker.KnownValues.ConvertPartition, IconSettings.IconAndText),
VisibleWhen = ActivationConstraint.SolutionState(SolutionState.FullyLoaded),
EnabledWhen = ActivationConstraint.ClientContext(ClientContextKey.Shell.ActiveEditorContentType, "csharp"),
};

/// <inheritdoc />
/// <remarks>
/// Get the active text and replace it by its equivalent in base64 or plain text.
/// </remarks>
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
using ITextViewSnapshot? textView = await context.GetActiveTextViewAsync(cancellationToken);
if (textView is null)
{
this.logger.TraceInformation("There was no active text view when command is executed.");
return;
}

await this.Extensibility.Editor().EditAsync(
batch =>
{
ITextDocumentEditor textDocumentEditor = textView.Document.AsEditable(batch);

var selections = textView.Selections;

for (int i = 0; i < selections.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var selection = selections[i];
if (selection.IsEmpty)
{
continue;
}

// For each selection, we will replace the selected text with its base64 or plain text equivalent
string newText = this.EncodeOrDecode(selection.Extent.CopyToString());
textDocumentEditor.Replace(selection.Extent, newText);
}
},
cancellationToken);
}

private string EncodeOrDecode(string text)
{
// Try to decode the string, maybe it's Base64?
Span<byte> buffer = stackalloc byte[text.Length];
if (Convert.TryFromBase64String(text, buffer, out int bytesWritten))
{
return Encoding.UTF8.GetString(buffer.Slice(0, bytesWritten));
}
else
{
// Seems like the input was not Base64, let's try to encode it to Base64 then.
byte[] data = Encoding.UTF8.GetBytes(text);
return Convert.ToBase64String(data);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace EncodeDecodeBase64;

using Microsoft.VisualStudio.Extensibility;

/// <summary>
/// Extension entry point for the EncodeDecodeBase64 extension.
/// </summary>
[VisualStudioContribution]
internal class EncodeDecodeBase64Extension : Extension
{
/// <inheritdoc/>
public override ExtensionConfiguration ExtensionConfiguration => new()
{
Metadata = new(
id: "EncodeDecodeBase64.87525EE0-4B75-4DF5-BB0E-C9EA1A5D2E15",
version: this.ExtensionAssemblyVersion,
publisherName: "Microsoft",
displayName: "Encode or Decode Base64 Sample Extension",
description: "Sample extension demonstrating encoding and decoding base64 text in the current document"),
};
}
90 changes: 90 additions & 0 deletions New_Extensibility_Model/Samples/EncodeDecodeBase64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Encode / Decode Base64 Extension Sample reference
description: A reference for Encode / Decode Base64 sample
date: 2025-4-3
---

# Walkthrough: Encode / Decode Base64 Extension Sample

This extension is a simple extension that shows how a command that modifies an open editor window can be quickly added to Visual Studio.

## Command definition

The extension contains a code file that defines a command and its properties starting with the `VisualStudioContribution` class attribute which makes the command available to Visual Studio:

```csharp
[VisualStudioContribution]
internal class EncodeDecodeBase64Command : Command
{
```

The `VisualStudioContribution` attribute registers the command using the class full type name `EncodeDecodeBase64.EncodeDecodeBase64Command` as its unique identifier.

The `CommandConfiguration` property defines information about the command that are available to Visual Studio even before the extension is loaded:

```csharp
public override CommandConfiguration CommandConfiguration => new("%EncodeDecodeBase64.EncodeDecodeBase64Command.DisplayName%")
{
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
Icon = new(ImageMoniker.KnownValues.ConvertPartition, IconSettings.IconAndText),
VisibleWhen = ActivationConstraint.SolutionState(SolutionState.FullyLoaded),
EnabledWhen = ActivationConstraint.ClientContext(ClientContextKey.Shell.ActiveEditorContentType, "csharp"),
};
```

The command is placed in `Extensions` top menu and uses `ConvertPartition` icon moniker.

The `VisibleWhen` and `EnabledWhen` properties defines when the command is visible and enabled in the `Extensions` menu. You can refer to [Activation Constraints](https://learn.microsoft.com/visualstudio/extensibility/visualstudio.extensibility/inside-the-sdk/activation-constraints) to learn about different options that you can use to determine command visibility and state. In this case, the command is visible anytime a solution is fully loaded in the IDE, but enabled only when the active editor is a C# document.

## Getting the active editor view

Once user executes the command, SDK will route execution to `ExecuteCommandAsync` method. `IClientContext` instance contains information about the state of IDE at the time of command execution and can be used in conjunction with `VisualStudioExtensibility` object.

In our example, we utilize `GetActiveTextViewAsync` method to get the active text view at the time of command execution which includes information about document being open, version of the document and the selection.

```csharp
using var textView = await context.GetActiveTextViewAsync(cancellationToken);
```

## Mutating the text in active view

Once we have the active text view, we can edit the document attached to the view to replace the selection with a new guid string as below.

```csharp
ITextViewSnapshot textDocumentEditor = await textView.GetTextDocumentAsync(cancellationToken);
await this.Extensibility.Editor().EditAsync(
batch =>
{
ITextDocumentEditor textDocumentEditor = textView.Document.AsEditable(batch);
// [...]
},
cancellationToken);
```

## Accessing every selections in the active view

The `ITextViewSnapshot` instance contains a `Selections` property that can be used to get the current selections in the view. There can be empty or [multiple selections](https://learn.microsoft.com/en-us/visualstudio/ide/finding-and-replacing-text?view=vs-2022#multi-caret-selection).
```csharp
ITextDocumentEditor textDocumentEditor = textView.Document.AsEditable(batch);
var selections = textView.Selections;

for (int i = 0; i < selections.Count; i++)
{
var selection = selections[i];
if (selection.IsEmpty)
{
continue;
}

string newText = this.EncodeOrDecode(selection.Extent.CopyToString());
textDocumentEditor.Replace(selection.Extent, newText);
}
```

## Logging errors

Each extension part including command sets is assigned a `TraceSource` instance that can be utilized to log diagnostic errors. Please see [Logging](https://learn.microsoft.com/visualstudio/extensibility/visualstudio.extensibility/inside-the-sdk/logging) section for more information.

## Usage

Once deployed, the Encode / Decode Base64 command can be used when editing any C# document. The command by default will be under Extensions menu.
6 changes: 6 additions & 0 deletions New_Extensibility_Model/Samples/Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompositeExtension", "Compo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaggersSample", "TaggersSample\TaggersSample.csproj", "{F7631680-13DF-42EC-AA87-C29FD4BE2273}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EncodeDecodeBase64", "EncodeDecodeBase64\EncodeDecodeBase64.csproj", "{4E6E513B-0690-4B75-8F45-8DFB73D630CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -185,6 +187,10 @@ Global
{F7631680-13DF-42EC-AA87-C29FD4BE2273}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7631680-13DF-42EC-AA87-C29FD4BE2273}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7631680-13DF-42EC-AA87-C29FD4BE2273}.Release|Any CPU.Build.0 = Release|Any CPU
{4E6E513B-0690-4B75-8F45-8DFB73D630CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E6E513B-0690-4B75-8F45-8DFB73D630CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E6E513B-0690-4B75-8F45-8DFB73D630CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E6E513B-0690-4B75-8F45-8DFB73D630CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down