Add ability to save input action from command line (#16513)

Hi wanted to make an attempt at
[12857](https://github.com/microsoft/terminal/issues/12857). This still
needs work but I think the initial version is ready to be reviewed.

## Summary of the Pull Request

Mostly copied from:
6f5b9fb...1cde67ac46

- Save to disk
- If command line is empty use selection
- Show toast
- No UI.  Trying out the different options now.

## PR Checklist
- [ ] Closes #12857
- [ ] Tests added/passed
- [ ] Documentation updated
- If checked, please file a pull request on [our docs
repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
- [ ] Schema updated (if necessary)

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
e82eric
2024-07-02 07:27:20 -04:00
committed by GitHub
parent 8009b53819
commit d051f7047d
18 changed files with 357 additions and 33 deletions

View File

@@ -1264,6 +1264,113 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::_HandleSaveSnippet(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if constexpr (!Feature_SaveSnippet::IsEnabled())
{
return;
}
if (args)
{
if (const auto& realArgs = args.ActionArgs().try_as<SaveSnippetArgs>())
{
auto commandLine = realArgs.Commandline();
if (commandLine.empty())
{
if (const auto termControl{ _GetActiveControl() })
{
if (termControl.HasSelection())
{
const auto selections{ termControl.SelectedText(true) };
const auto selection = std::accumulate(selections.begin(), selections.end(), std::wstring());
commandLine = selection;
}
}
}
if (commandLine.empty())
{
ActionSaveFailed(L"CommandLine is Required");
return;
}
try
{
KeyChord keyChord = nullptr;
if (!realArgs.KeyChord().empty())
{
keyChord = KeyChordSerialization::FromString(winrt::to_hstring(realArgs.KeyChord()));
}
_settings.GlobalSettings().ActionMap().AddSendInputAction(realArgs.Name(), commandLine, keyChord);
_settings.WriteSettingsToDisk();
ActionSaved(commandLine, realArgs.Name(), realArgs.KeyChord());
}
catch (const winrt::hresult_error& ex)
{
auto code = ex.code();
auto message = ex.message();
ActionSaveFailed(message);
args.Handled(true);
return;
}
args.Handled(true);
}
}
}
void TerminalPage::ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord)
{
// If we haven't ever loaded the TeachingTip, then do so now and
// create the toast for it.
if (_actionSavedToast == nullptr)
{
if (auto tip{ FindName(L"ActionSavedToast").try_as<MUX::Controls::TeachingTip>() })
{
_actionSavedToast = std::make_shared<Toast>(tip);
// Make sure to use the weak ref when setting up this
// callback.
tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl });
}
}
_UpdateTeachingTipTheme(ActionSavedToast().try_as<winrt::Windows::UI::Xaml::FrameworkElement>());
SavedActionName(name);
SavedActionKeyChord(keyChord);
SavedActionCommandLine(input);
if (_actionSavedToast != nullptr)
{
_actionSavedToast->Open();
}
}
void TerminalPage::ActionSaveFailed(winrt::hstring message)
{
// If we haven't ever loaded the TeachingTip, then do so now and
// create the toast for it.
if (_actionSaveFailedToast == nullptr)
{
if (auto tip{ FindName(L"ActionSaveFailedToast").try_as<MUX::Controls::TeachingTip>() })
{
_actionSaveFailedToast = std::make_shared<Toast>(tip);
// Make sure to use the weak ref when setting up this
// callback.
tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl });
}
}
_UpdateTeachingTipTheme(ActionSaveFailedToast().try_as<winrt::Windows::UI::Xaml::FrameworkElement>());
ActionSaveFailedMessage().Text(message);
if (_actionSaveFailedToast != nullptr)
{
_actionSaveFailedToast->Open();
}
}
void TerminalPage::_HandleSelectCommand(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

View File

@@ -209,6 +209,7 @@ void AppCommandlineArgs::_buildParser()
_buildMovePaneParser();
_buildSwapPaneParser();
_buildFocusPaneParser();
_buildSaveSnippetParser();
}
// Method Description:
@@ -537,6 +538,72 @@ void AppCommandlineArgs::_buildFocusPaneParser()
setupSubcommand(_focusPaneShort);
}
void AppCommandlineArgs::_buildSaveSnippetParser()
{
_saveCommand = _app.add_subcommand("x-save-snippet", RS_A(L"SaveSnippetDesc"));
auto setupSubcommand = [this](auto* subcommand) {
subcommand->add_option("--name,-n", _saveInputName, RS_A(L"SaveSnippetArgDesc"));
subcommand->add_option("--keychord,-k", _keyChordOption, RS_A(L"KeyChordArgDesc"));
subcommand->add_option("command,", _commandline, RS_A(L"CmdCommandArgDesc"));
subcommand->positionals_at_end(true);
// When ParseCommand is called, if this subcommand was provided, this
// callback function will be triggered on the same thread. We can be sure
// that `this` will still be safe - this function just lets us know this
// command was parsed.
subcommand->callback([&, this]() {
// Build the action from the values we've parsed on the commandline.
ActionAndArgs saveSnippet{};
saveSnippet.Action(ShortcutAction::SaveSnippet);
// First, parse out the commandline in the same way that
// _getNewTerminalArgs does it
SaveSnippetArgs args{};
if (!_commandline.empty())
{
std::ostringstream cmdlineBuffer;
for (const auto& arg : _commandline)
{
if (cmdlineBuffer.tellp() != 0)
{
// If there's already something in here, prepend a space
cmdlineBuffer << ' ';
}
if (arg.find(" ") != std::string::npos)
{
cmdlineBuffer << '"' << arg << '"';
}
else
{
cmdlineBuffer << arg;
}
}
args.Commandline(winrt::to_hstring(cmdlineBuffer.str()));
}
if (!_keyChordOption.empty())
{
args.KeyChord(winrt::to_hstring(_keyChordOption));
}
if (!_saveInputName.empty())
{
winrt::hstring hString = winrt::to_hstring(_saveInputName);
args.Name(hString);
}
saveSnippet.Args(args);
_startupActions.push_back(saveSnippet);
});
};
setupSubcommand(_saveCommand);
}
// Method Description:
// - Add the `NewTerminalArgs` parameters to the given subcommand. This enables
// that subcommand to support all the properties in a NewTerminalArgs.
@@ -710,7 +777,8 @@ bool AppCommandlineArgs::_noCommandsProvided()
*_focusPaneCommand ||
*_focusPaneShort ||
*_newPaneShort.subcommand ||
*_newPaneCommand.subcommand);
*_newPaneCommand.subcommand ||
*_saveCommand);
}
// Method Description:

View File

@@ -93,6 +93,7 @@ private:
CLI::App* _swapPaneCommand;
CLI::App* _focusPaneCommand;
CLI::App* _focusPaneShort;
CLI::App* _saveCommand;
// Are you adding a new sub-command? Make sure to update _noCommandsProvided!
@@ -123,6 +124,8 @@ private:
bool _focusPrevTab{ false };
int _focusPaneTarget{ -1 };
std::string _saveInputName;
std::string _keyChordOption;
// Are you adding more args here? Make sure to reset them in _resetStateToDefault
const Commandline* _currentCommandline{ nullptr };
@@ -141,6 +144,7 @@ private:
winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs _getNewTerminalArgs(NewTerminalSubcommand& subcommand);
void _addNewTerminalArgs(NewTerminalSubcommand& subcommand);
void _buildParser();
void _buildSaveSnippetParser();
void _buildNewTabParser();
void _buildSplitPaneParser();
void _buildFocusTabParser();

View File

@@ -288,6 +288,15 @@
<data name="CmdCommandArgDesc" xml:space="preserve">
<value>An optional command, with arguments, to be spawned in the new tab or pane</value>
</data>
<data name="SaveSnippetDesc" xml:space="preserve">
<value>Save command line as input action</value>
</data>
<data name="SaveSnippetArgDesc" xml:space="preserve">
<value>An optional argument</value>
</data>
<data name="KeyChordArgDesc" xml:space="preserve">
<value>An optional argument</value>
</data>
<data name="CmdFocusTabDesc" xml:space="preserve">
<value>Move focus to another tab</value>
</data>
@@ -898,4 +907,10 @@
<data name="RestartConnectionToolTip" xml:space="preserve">
<value>Restart the active pane connection</value>
</data>
<data name="ActionSavedToast.Title" xml:space="preserve">
<value>Action saved</value>
</data>
<data name="ActionSaveFailedToast.Title" xml:space="preserve">
<value>Action save failed</value>
</data>
</root>

View File

@@ -357,7 +357,9 @@
</ItemGroup>
<!-- ========================= Misc Files ======================== -->
<ItemGroup>
<PRIResource Include="Resources\en-US\Resources.resw" />
<PRIResource Include="Resources\en-US\Resources.resw">
<SubType>Designer</SubType>
</PRIResource>
<PRIResource Include="Resources\en-US\ContextMenu.resw" />
<OCResourceDirectory Include="Resources" />
</ItemGroup>
@@ -466,10 +468,8 @@
</ItemDefinitionGroup>
<!-- ========================= Globals ======================== -->
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<!-- This -must- go after cppwinrt.build.post.props because that includes many VS-provided props including appcontainer.common.props, which stomps on what cppwinrt.targets did. -->
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />
<!--
By default, the PRI file will contain resource paths beginning with the
project name. Since we enabled XBF embedding, this *also* includes App.xbf.
@@ -490,4 +490,4 @@
</ItemGroup>
</Target>
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
</Project>
</Project>

View File

@@ -146,6 +146,8 @@ namespace winrt::TerminalApp::implementation
winrt::hstring KeyboardServiceDisabledText();
winrt::fire_and_forget IdentifyWindow();
void ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord);
void ActionSaveFailed(winrt::hstring message);
winrt::fire_and_forget RenameFailed();
winrt::fire_and_forget ShowTerminalWorkingDirectory();
@@ -199,6 +201,10 @@ namespace winrt::TerminalApp::implementation
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr);
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr);
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionName, PropertyChanged.raise, L"");
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionKeyChord, PropertyChanged.raise, L"");
WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionCommandLine, PropertyChanged.raise, L"");
private:
friend struct TerminalPageT<TerminalPage>; // for Xaml to bind events
std::optional<HWND> _hostingHwnd;
@@ -258,6 +264,8 @@ namespace winrt::TerminalApp::implementation
bool _isEmbeddingInboundListener{ false };
std::shared_ptr<Toast> _windowIdToast{ nullptr };
std::shared_ptr<Toast> _actionSavedToast{ nullptr };
std::shared_ptr<Toast> _actionSaveFailedToast{ nullptr };
std::shared_ptr<Toast> _windowRenameFailedToast{ nullptr };
std::shared_ptr<Toast> _windowCwdToast{ nullptr };

View File

@@ -72,6 +72,10 @@ namespace TerminalApp
void IdentifyWindow();
void RenameFailed();
String SavedActionName { get; };
String SavedActionKeyChord { get; };
String SavedActionCommandLine { get; };
// We cannot use the default XAML APIs because we want to make sure
// that there's only one application-global dialog visible at a time,
// and because of GH#5224.

View File

@@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
Background="Transparent"
mc:Ignorable="d">
@@ -204,5 +205,43 @@
Title="{x:Bind WindowProperties.VirtualWorkingDirectory, Mode=OneWay}"
x:Load="False"
IsLightDismissEnabled="True" />
<mux:TeachingTip x:Name="ActionSavedToast"
x:Uid="ActionSavedToast"
Title="Action Saved"
HorizontalAlignment="Stretch"
x:Load="False"
IsLightDismissEnabled="True">
<mux:TeachingTip.Content>
<StackPanel HorizontalAlignment="Stretch"
Orientation="Vertical">
<TextBlock x:Name="ActionSavedNameText"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(SavedActionName), Mode=OneWay}">
<Run Text="Name: " />
<Run Text="{x:Bind SavedActionName, Mode=OneWay}" />
</TextBlock>
<TextBlock x:Name="ActionSavedKeyChordText"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(SavedActionKeyChord), Mode=OneWay}">
<Run Text="Key Chord: " />
<Run Text="{x:Bind SavedActionKeyChord, Mode=OneWay}" />
</TextBlock>
<TextBlock x:Name="ActionSavedCommandLineText"
Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(SavedActionCommandLine), Mode=OneWay}">
<Run Text="Input: " />
<Run Text="{x:Bind SavedActionCommandLine, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</mux:TeachingTip.Content>
</mux:TeachingTip>
<mux:TeachingTip x:Name="ActionSaveFailedToast"
x:Uid="ActionSaveFailedToast"
Title="Action Save Failed"
x:Load="False"
IsLightDismissEnabled="True">
<mux:TeachingTip.Content>
<TextBlock x:Name="ActionSaveFailedMessage"
Text="" />
</mux:TeachingTip.Content>
</mux:TeachingTip>
</Grid>
</Page>

View File

@@ -52,6 +52,7 @@ static constexpr std::string_view SwitchToTabKey{ "switchToTab" };
static constexpr std::string_view TabSearchKey{ "tabSearch" };
static constexpr std::string_view ToggleAlwaysOnTopKey{ "toggleAlwaysOnTop" };
static constexpr std::string_view ToggleCommandPaletteKey{ "commandPalette" };
static constexpr std::string_view SaveSnippetKey{ "experimental.saveSnippet" };
static constexpr std::string_view SuggestionsKey{ "showSuggestions" };
static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" };
static constexpr std::string_view SetFocusModeKey{ "setFocusMode" };
@@ -389,6 +390,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::TabSearch, RS_(L"TabSearchCommandKey") },
{ ShortcutAction::ToggleAlwaysOnTop, RS_(L"ToggleAlwaysOnTopCommandKey") },
{ ShortcutAction::ToggleCommandPalette, MustGenerate },
{ ShortcutAction::SaveSnippet, MustGenerate },
{ ShortcutAction::Suggestions, MustGenerate },
{ ShortcutAction::ToggleFocusMode, RS_(L"ToggleFocusModeCommandKey") },
{ ShortcutAction::SetFocusMode, MustGenerate },

View File

@@ -33,6 +33,7 @@
#include "ScrollToMarkArgs.g.cpp"
#include "AddMarkArgs.g.cpp"
#include "FindMatchArgs.g.cpp"
#include "SaveSnippetArgs.g.cpp"
#include "ToggleCommandPaletteArgs.g.cpp"
#include "SuggestionsArgs.g.cpp"
#include "NewWindowArgs.g.cpp"
@@ -947,6 +948,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}
winrt::hstring SaveSnippetArgs::GenerateName() const
{
if (Feature_SaveSnippet::IsEnabled())
{
std::wstringstream ss;
ss << RS_(L"SaveSnippetNamePrefix").c_str() << L" commandline: " << Commandline().c_str();
if (!Name().empty())
{
ss << L", name: " << Name().c_str();
}
if (!KeyChord().empty())
{
ss << L", keyChord " << KeyChord().c_str();
}
return winrt::hstring{ ss.str() };
}
return L"";
}
static winrt::hstring _FormatColorString(const Control::SelectionColor& selectionColor)
{
if (!selectionColor)

View File

@@ -34,6 +34,7 @@
#include "ScrollToMarkArgs.g.h"
#include "AddMarkArgs.g.h"
#include "MoveTabArgs.g.h"
#include "SaveSnippetArgs.g.h"
#include "ToggleCommandPaletteArgs.g.h"
#include "SuggestionsArgs.g.h"
#include "FindMatchArgs.g.h"
@@ -215,6 +216,12 @@ protected: \
#define TOGGLE_COMMAND_PALETTE_ARGS(X) \
X(CommandPaletteLaunchMode, LaunchMode, "launchMode", false, CommandPaletteLaunchMode::Action)
////////////////////////////////////////////////////////////////////////////////
#define SAVE_TASK_ARGS(X) \
X(winrt::hstring, Name, "name", false, L"") \
X(winrt::hstring, Commandline, "commandline", args->Commandline().empty(), L"") \
X(winrt::hstring, KeyChord, "keyChord", false, L"")
////////////////////////////////////////////////////////////////////////////////
#define SUGGESTIONS_ARGS(X) \
X(SuggestionsSource, Source, "source", false, SuggestionsSource::Tasks) \
@@ -819,6 +826,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ACTION_ARGS_STRUCT(ToggleCommandPaletteArgs, TOGGLE_COMMAND_PALETTE_ARGS);
ACTION_ARGS_STRUCT(SaveSnippetArgs, SAVE_TASK_ARGS);
ACTION_ARGS_STRUCT(SuggestionsArgs, SUGGESTIONS_ARGS);
ACTION_ARGS_STRUCT(FindMatchArgs, FIND_MATCH_ARGS);
@@ -941,6 +950,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
BASIC_FACTORY(CloseTabArgs);
BASIC_FACTORY(MoveTabArgs);
BASIC_FACTORY(OpenSettingsArgs);
BASIC_FACTORY(SaveSnippetArgs);
BASIC_FACTORY(FindMatchArgs);
BASIC_FACTORY(NewWindowArgs);
BASIC_FACTORY(FocusPaneArgs);

View File

@@ -357,6 +357,15 @@ namespace Microsoft.Terminal.Settings.Model
FindMatchDirection Direction { get; };
};
[default_interface] runtimeclass SaveSnippetArgs : IActionArgs
{
SaveSnippetArgs();
SaveSnippetArgs(String Name, String Commandline, String KeyChord);
String Name;
String Commandline;
String KeyChord;
};
[default_interface] runtimeclass NewWindowArgs : IActionArgs
{
NewWindowArgs(INewContentArgs contentArgs);

View File

@@ -882,6 +882,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return results;
}
void ActionMap::AddSendInputAction(winrt::hstring name, winrt::hstring input, const Control::KeyChord keys)
{
auto newAction = winrt::make<ActionAndArgs>();
newAction.Action(ShortcutAction::SendInput);
auto sendInputArgs = winrt::make<SendInputArgs>(input);
newAction.Args(sendInputArgs);
auto cmd{ make_self<Command>() };
if (!name.empty())
{
cmd->Name(name);
}
cmd->ActionAndArgs(newAction);
cmd->GenerateID();
AddAction(*cmd, keys);
}
IVector<Model::Command> ActionMap::FilterToSendInput(
winrt::hstring currentCommandline)
{

View File

@@ -78,6 +78,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
bool RebindKeys(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys);
void DeleteKeyBinding(const Control::KeyChord& keys);
void RegisterKeyBinding(Control::KeyChord keys, Model::ActionAndArgs action);
void AddSendInputAction(winrt::hstring name, winrt::hstring input, const Control::KeyChord keys);
Windows::Foundation::Collections::IVector<Model::Command> ExpandedCommands();
void ExpandCommands(const Windows::Foundation::Collections::IVectorView<Model::Profile>& profiles,

View File

@@ -31,5 +31,6 @@ namespace Microsoft.Terminal.Settings.Model
void DeleteKeyBinding(Microsoft.Terminal.Control.KeyChord keys);
void RegisterKeyBinding(Microsoft.Terminal.Control.KeyChord keys, ActionAndArgs action);
void AddSendInputAction(String name, String input, Microsoft.Terminal.Control.KeyChord keys);
}
}

View File

@@ -75,6 +75,7 @@
ON_ALL_ACTIONS(CloseTabsAfter) \
ON_ALL_ACTIONS(TabSearch) \
ON_ALL_ACTIONS(MoveTab) \
ON_ALL_ACTIONS(SaveSnippet) \
ON_ALL_ACTIONS(BreakIntoDebugger) \
ON_ALL_ACTIONS(TogglePaneReadOnly) \
ON_ALL_ACTIONS(EnablePaneReadOnly) \
@@ -148,6 +149,7 @@
ON_ALL_ACTIONS_WITH_ARGS(SplitPane) \
ON_ALL_ACTIONS_WITH_ARGS(SwitchToTab) \
ON_ALL_ACTIONS_WITH_ARGS(ToggleCommandPalette) \
ON_ALL_ACTIONS_WITH_ARGS(SaveSnippet) \
ON_ALL_ACTIONS_WITH_ARGS(FocusPane) \
ON_ALL_ACTIONS_WITH_ARGS(ExportBuffer) \
ON_ALL_ACTIONS_WITH_ARGS(ClearBuffer) \

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -727,4 +727,7 @@
<value>Open about dialog</value>
<comment>This will open the "about" dialog, to display version info and other documentation</comment>
</data>
</root>
<data name="SaveSnippetNamePrefix" xml:space="preserve">
<value>Save Snippet</value>
</data>
</root>

View File

@@ -155,6 +155,17 @@
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_SaveSnippet</name>
<description>Save Snippet</description>
<id>9971</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
<brandingToken>Canary</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_QuickFix</name>
<description>Enables the Quick Fix menu</description>