diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/.vsextension/string-resources.json b/New_Extensibility_Model/Samples/EncodeDecodeBase64/.vsextension/string-resources.json new file mode 100644 index 00000000..b71b6997 --- /dev/null +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/.vsextension/string-resources.json @@ -0,0 +1,3 @@ +{ + "EncodeDecodeBase64.EncodeDecodeBase64Command.DisplayName": "Encode / Decode Base64" +} \ No newline at end of file diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj new file mode 100644 index 00000000..c8e240df --- /dev/null +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj @@ -0,0 +1,13 @@ + + + net8.0-windows8.0 + enable + 12 + en-US + + + + + + + diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Command.cs b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Command.cs new file mode 100644 index 00000000..a59d3a33 --- /dev/null +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Command.cs @@ -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; + +/// +/// Command handler to encode or decode a base64 text from the currently selected text. +/// +/// +/// Initializes a new instance of the class. +/// +/// Trace source instance to utilize. +[VisualStudioContribution] +internal class EncodeDecodeBase64Command(TraceSource traceSource) : Command +{ + private readonly TraceSource logger = traceSource; + + /// + 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"), + }; + + /// + /// + /// Get the active text and replace it by its equivalent in base64 or plain text. + /// + 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 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); + } + } +} diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Extension.cs b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Extension.cs new file mode 100644 index 00000000..3851048f --- /dev/null +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64Extension.cs @@ -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; + +/// +/// Extension entry point for the EncodeDecodeBase64 extension. +/// +[VisualStudioContribution] +internal class EncodeDecodeBase64Extension : Extension +{ + /// + 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"), + }; +} diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/README.md b/New_Extensibility_Model/Samples/EncodeDecodeBase64/README.md new file mode 100644 index 00000000..8d869977 --- /dev/null +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/README.md @@ -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. diff --git a/New_Extensibility_Model/Samples/Samples.sln b/New_Extensibility_Model/Samples/Samples.sln index 0c6aa5f1..5741aef0 100644 --- a/New_Extensibility_Model/Samples/Samples.sln +++ b/New_Extensibility_Model/Samples/Samples.sln @@ -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 @@ -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