[搬运] 将 Visual Studio 的代码片段导出到 VS Code

原文 : A Visual Studio to Visual Studio Code Snippet Converter
作者 : Rick Strahl
译者 : 张蘅水

导语

和原文作者一样,水弟我现在也是使用 VS Code 和 Rider 作为主力开发工具,尤其是 VS Code 可以跨平台,又有丰富的插件支持和多种编程语言支持。当我从 VS 转移到以 VS Code 的开发过程中,遇到的最大问题就是代码提示的不完善(被 VS 和 R# 调教坏了,总想按 tab 键)。当我看到原文作者通过从 VS 中导出代码片段到 VS Code 时,瞬间被吸引到了。 虽然在不知不觉中用了 VS 自带的代码片段,但我从来没有想过要自定义专属的代码片段,最多也是制作用于项目的模板(方便创建特定的类型文件)。虽然导出到 Rider 不是很完美,但 Rider 自带了 R#,对这方面的需求还是很少的。


译文:

Visual Studio 内置了非常好用的代码片段工具,多年来我一直在使用它来创建大量有用的扩展片段,使我的日常开发更容易。我有很多 C# 代码片段,但更多的是用于 HTML 、自定义的 Bootstrap 代码片段,乃至复杂的 HTML 控件代码段。偶尔也会用到 JavaScript 、XAML 甚至Powershell 。

在过去的几年里,我越来越多地使用其他工具与 Visual Studio 结合使用。特别是Visual Studio CodeJetBrains Rider

在多年的使用 Visual Studio 中,我已经累积了 130 多个代码片段。每当我在其他开发环境中工作时 ( VS Code 或者 Rider),我真的很需要他们,特别是要写一大段 HTML的时候,总是要去痛苦地去对应的文档站点查找。使用代码片段功能,只需几次击键就会自动填充我自定义的特定代码,每天可节省大量时间。

所以我很需要代码片段功能,有时我打开 Visual Studio 只是为了找到需要的 HTML 的代码片段,然后将它们粘贴回 VS Code 或 Rider。虽然繁琐,但是仍然从文档网站中复制代码,然后手动修改代码来得便捷。如果能在每个对应的开发环境中直接执行代码片段的功能,那就太好了!

因此,在过去的几个周末,我做了一个将 Visual Studio 中的代码片段导出到 VS Code 中的小工具,同时尽量能导出到 JetBrains Rider 。

如果你感兴趣,可以在GitHub上找到代码:

另外说一句,这还只是一个菜鸟项目,并不能保证它支持所有类型的的代码片段。只是我自己拥有的 137 个代码片段都完美地移植到 VS Code,并且能够运行。同时我还可以重新导出, 轻松地导出新创建的代码片段,这样就可以对比和更新了。

对于 Rider 而言,操作起来更为复杂,因为 Rider 有一种疯狂的机制,可以将模板存储在内部的单个配置文件中。它还为 .NET相关的片段 (C#、VB、F#、Razor、ASP.NET )和 基于 Web ( html、css、js 等)的代码片段使用了多个完全不同的存储引擎。所以工具目前仅支持一次性导出 .NET 相关代码段,因为 Rider 中基于 GUID 的密钥系统不允许在没有 GUID 的情况下查找现有代码段。后面我们再详细介绍。

代码片段转换器

The Snippet Converter

你可以通过借助 .NET 全局工具 (.NET Global SDK Tool ),使用 Nuget 下载和运行代码片段转换器:

dotnet tool install --global dotnet-snippetconverter

如果您不想安装并只运行该工具,您可以克隆或下载Github仓库,然后:

cd .\SnippetConverter\
dotnet run

安装后, 可以通过指向文件夹或单个文件将 Visual Studio 中的代码片段批量或单独转换为 VS Code 支持的代码片段。

snippetconverter ~2017 -r -d

或者,您可以像下面这张屏幕截图那样指定输出文件:

image

有几个选项可用于转换单个片段和文件夹,使用前缀,递归文件夹,输出生成文件的路径等:

Syntax:
-------
SnippetConverter <sourceFileOrDirectory> -o <outputFile> 
                 --mode --prefix --recurse --display

Commands:
---------
HELP || /?          This help display           

Options:
--------
sourceFileOrDirectory  Either an individual snippet file, or a source folder
                       Optional special start syntax using `~` to point at User Code Snippets folder:
                       ~      -  Visual Studio User Code Snippets folder (latest version installed)
                       ~2017  -  Visual Studio User Code Snippets folder (specific VS version 2019-2012)                       

-o <outputFile>        Output file where VS Code snippets are generated into (ignored by Rider)   
                       Optional special start syntax using `~` to point at User Code Snippets folder:
                       %APPDATA%\Code\User\snippets\ww-my-codesnippets.code-snippets
                       ~\ww-my-codesnippets.code-snippets
                       if omitted generates `~\exported-visualstudio.code-snippets`
                       
-m,--mode              vs-vscode  (default)
                       vs-rider   experimental - (C#,VB.NET,html only)
-d                     display the target file in Explorer
-r                     if specifying a source folder recurses into child folders
-p,--prefix            snippet prefix generate for all snippets exported
                       Example: `ww-` on a snippet called `ifempty` produces `ww-ifempty`

Examples:
---------
# vs-vscode: Individual Visual Studio Snippet
SnippetConverter "~2017\Visual C#\My Code Snippets\proIPC.snippet" 
                 -o "~\ww-csharp.code-snippets" -d

# vs-vscode: All snippets in a folder user VS Snippets and in recursive child folers
SnippetConverter "~2017\Visual C#\My Code Snippets" -o "~\ww-csharp.code-snippets" -r -d

# vs-vscode: All the user VS Snippets and in recursive child folders
SnippetConverter ~2017\ -o "~\ww-all.code-snippets" -r -d

# vs-vscode: All defaults: Latest version of VS, all snippets export to  ~\visualstudio-export.code-snippets
SnippetConverter ~ -r -d --prefix ww-

# vs-rider: Individual VS Snippet
SnippetConverter "~2017\proIPC.snippet" -m vs-rider -d

# vs-rider: All VS Snippets in a folder
SnippetConverter "~2017\Visual C#\My Code Snippets" -m vs-rider -d

上面的用例应该足够说明用途了。如果还想要了解更多信息,请接着往下看......

什么是 VS Code 的代码片段

如果您不熟悉或不使用代码片段,那您并不是少数人。它们在 Visual Studio 中几乎是一个隐藏的功能,这是一个耻辱,因为它们是非常有用的生产力工具。不幸的是,Visual Studio 没有任何有用的内置UI来创建这些片段,因此大多数开发人员都没有充分利用此功能。Visual Studio 只能蹩脚地点击 ** 工具 - > 代码片段管理器 ** 菜单 ,除了一个查看器之外,它没有其他管理功能,仅仅是查看哪些片段是可用的,没有内置的方法来创建或编辑片段,甚至跳转到并查看代码片段。

但是,代码片段仅仅只是位于用户目录的 Documents 文件夹下的 XML 文件。它们非常容易创建和更新,仅仅是原始的 XML 文件,用 VS Code 等文本编辑器去做代码片段和高亮实在是非常简单。尽管在 Visual Studio 中有一些提供 UI 操作的劣质插件,但它们往往比原始的代码片段文件更麻烦。

创建新代码段的最佳方法是复制现有代码段并对其进行修改以满足您的需求。

一般来说,代码片段位于 (水弟我是直接用 Everything搜索的):

<Documents>\Visual Studio 2017\Code Snippets

每种语言技术都有自己的子文件夹进行分组,但仅仅是文件夹上的区分而已。代码片段实际上通过 XML中的 Language 属性确定它们适用的语言。

Visual Studio在此位置附带了许多代码段,您可以使用这些代码段作为新代码段的模板进行学习。

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Property with INotifyPropertyChange raised</Title>
      <Description>Control Property with Attributes</Description>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
      <Shortcut>proIPC</Shortcut>
    </Header>
    <Snippet>
      <References />
      <Imports />
      <Declarations>
        <Literal Editable="true">
          <ID>name</ID>
          <Type></Type>
          <ToolTip>Property Name</ToolTip>
          <Default>MyProperty</Default>
          <Function></Function>
        </Literal>        
        <Literal Editable="true">
          <ID>type</ID>
          <Type></Type>
          <ToolTip>Property Type</ToolTip>
          <Default>string</Default>
          <Function></Function>
        </Literal>
      </Declarations>
      <Code Language="csharp" Kind="method decl" Delimiter="$"><![CDATA[public $type$ $name$
{
    get { return _$name$; }
    set
    {
        if (value == _$name$) return;
        _$name$ = value;
        OnPropertyChanged(nameof($name$));
    }
}        
private $type$ _$name$;
]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

一旦文件存在或更新了,Visual Studio 无需重启,就能立即发现并使用。在相关的 (如 C#) 编辑器中,立马就能看到智能提示中的代码段:

智能提示

它插入对应的模板并允许您编辑在模板中声明的 $expr$ 占位符:

EditSnippetInVS.png

这个 C# 代码片段示例, 是 VS 中最常见的语言。如您所见, XML文件中的 <Code> 节点定义模板文本,Shortcut 节点定义触发提示的按键,你可以使用在 <Declaration> 节点中使用类似 $txt$ 占位符来定义参数,同样的占位符在多个地方出现也能同步更改。

对于我来说,最有用和最常用的代码片段时用于插入 HTML 代码,特别是在定义 Bootstrap 结构或其他很难记住语法的自定义控件。我喜欢在浏览文档网站后创建一个对应的片段,这样就很方便使用。用多几次,省下来的时间就赚翻了。花费几分钟设置模板可以节省大量时间去输入重复代码,尤其是您每次都要浪费时间查找相同的 Bootstrap 代码时。😃

前缀以及代码片段包

Visual Studio Marketplace中还有许多可用的代码片段,您可以安装使用一整套预设的代码片段。例如,Bootstrap Snippet 包就内置了一堆以 bs- 为前缀的代码片段。

代码片段包

即使您自己有专属的代码片段,最好为您的代码片段创建一个前缀,以便您可以在智能提示的海洋中轻松地找到它们。我一般使用 ww- 作为大多数代码片段的前缀。不幸的是,我自己没有很好得遵循这个建议,还是有不少代码片段没有这么做。

构建转换器

因为我在 Visual Studio 大量使用了代码片段,所以我做了一个将 Visual Studio 中的代码片段迁移到 VS Code 中的小工具,同时尽量能迁移到 JetBrains Rider 。

我想可能还有其他人需要用到,所以我把它作为 .NET Global Tool 控制台应用程序发布,以便快速安装:

dotnet tool install dotnet-snippetconverter

您需要.NET Core 2.1 SDK或更高版本才能运行它。

以下示例命令将代码片段从 Visual Studio 迁移到 VS Code,稍后再讨论迁移到 Rider 的事

安装后,您可以使用以下命令快速将所有 Visual Studio 代码片段转换为 VS Code 可以接受的格式。

snippetconverter ~ -r -d 

这将转换最新安装的 Visual Studio 版本(2017,2019等)中的所有代码片段,并在位于%appdata%\Code\User\snippets 路径的 VS Code 的代码文件夹中创建单独的 visualstudio-exported.code-snippets 文件夹。

您还可以导出特定 VS 版本的代码片段:

snippetconverter ~2017 -r -d

或特定文件夹:

snippetconverter "~2017\Visual C#\My Code Snippets" -r -d -o "~\ww-csharp.code-snippets"

其中输入和输出文件夹选项中的路径都是可选的,示例中的~ 是物理片段路径的占位符,会指向 Visual Studio(%Documents%\Visual Studio <year>\Code Snippets)和 VS Code(%appdata%\Code\User\Snippets\)中存放代码片段的基本位置,因此您不必每次都指定完整路径。您高兴的话,也可以使用合格的全路径。

最后,您还可以导出单个文件:

snippetconverter "~2017\Visual C#\My Code Snippets\proIPC.snippet" -d -o "~\ww-csharp.code-snippets"

如果 VS Code 中已存在该代码片段,则会覆盖更新,所以每次重新运行都会更新对应的代码片段。

运行迁移工具后,在VS Code 中通过前缀或者快捷方式就可以立即使用:

在 Visual Studio 中多个占位符输入也是支持的:

同步代码片段

目前只支持从Visual Studio 单向 迁移到到 VS Code。这意味着如果要保持 Visual Studio 和 VS Code 之间的代码段同步,最好是在 Visual Studio 中创建代码片段,然后通过此工具将它们迁移到 VS Code。

VS Code 中的代码片段

我之前讨论过 Visual Studio Snippets 的代码片段格式,现在让我们看看 VS Code 中又是什么样的。

  • 存放在 %AppData\Code\User\snippets
  • 使用 JSON 格式化
  • 命名为 lang.json
  • 或者是 <name>.code-snippet 的命名格式
  • 可以包含一个或者多个代码片段

转换器之所以导出为 .code-snippet 文件格式,是因为使用 lang.json 很容易造成命名冲突。如果默认的 visualstudio-export.code-snippets 不能使用,则使用 -o 来指定输出文件。

VS Code 代码片段文件是 JSON,它们看起来像:

{
  "proipc": {
    "prefix": "proipc",
    "scope": "csharp",
    "body": [
      "public ${2:string} ${1:MyProperty}",
      "{",
      "    get { return _${1:MyProperty}; }",
      "    set",
      "    {",
      "        if (value == _${1:MyProperty}) return;",
      "        _${1:MyProperty} = value;",
      "        OnPropertyChanged(nameof(${1:MyProperty}));",
      "    }",
      "}        ",
      "private ${2:string} _${1:MyProperty};",
      ""
    ],
    "description": "Control Property with Attributes"
  },
  "commandbase-object-declaration": {
    "prefix": "commandbase",
    "scope": "csharp",
    "body": [
      "        public CommandBase ${1:CommandName}Command { get; set;  }",
      "",
      "        void Command_${1:CommandName}()",
      "        {",
      "            ${1:CommandName}Command = new CommandBase((parameter, command) =>",
      "            {",
      "              $0",
      "            }, (p, c) => true);",
      "        }",
      ""
    ],
    "description": "Create a CommandBase implementation and declaration"
  } 
}

VS Code 的代码模板在概念上更简单,只有模板,前缀和范围,以及使用字符串插值和约定来确定如何定义占位符。当然还有其他字段可以填充,但大多数值是可选的,对于从 Visual Studio 转换过来的代码片段用不到。

您可以在此处找到Visual Studio代码段模板文档:

但是实际上,自己手动创建模板,定义 JSON中的 body 属性还是有难度的,因为字符串可能只是一个字符串数组(yuk),也可能是一个可以输入的类型。好在只是从 Visual Studio 代码片段转换,还是很容易生成对应的模板...

咦?导出到 Rider

转换器某种程度上可以适配到 Rider,但功能有限。因为Rider 使用令人抓狂的模式来存储代码片段,使用 GUID 来标识的 XML 文件。

%USERPROFILE%\.Rider2018.2\config\resharper-host\GlobalSettingsStorage.DotSettings

让我们看看几个导出的模板效果:

<root>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Reformat/@EntryValue">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Shortcut/@EntryValue">proipc</s:String>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Text/@EntryValue">public $type$ $name$
{
    get { return _$name$; }
    set
    {
        if (value == _$name$) return;
        _$name$ = value;
        OnPropertyChanged(nameof($name$));
    }
}        
private $type$ _$name$;
    </s:String>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=name/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=name/Expression/@EntryValue">complete()</s:String>
    <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=name/Order/@EntryValue">0</s:Int64>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=type/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=type/Expression/@EntryValue">complete()</s:String>
    <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=720E28E0ECFD4CA0B80F10DC82149BD4/Field/=type/Order/@EntryValue">1</s:Int64>

    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/@KeyIndexDefined">True</s:Boolean>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Reformat/@EntryValue">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Shortcut/@EntryValue">seterror</s:String>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/ShortenQualifiedReferences/@EntryValue">True</s:Boolean>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue">InCSharpFile</s:String>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Text/@EntryValue">      
        public string ErrorMessage {get; set; }

        protected void SetError()
        {
            this.SetError("CLEAR");
        }

        protected void SetError(string message)
        {
            if (message == null || message=="CLEAR")
            {
                this.ErrorMessage = string.Empty;
                return;
            }
            this.ErrorMessage += message;
        }

        protected void SetError(Exception ex, bool checkInner = false)
        {
            if (ex == null)
                this.ErrorMessage = string.Empty;

            Exception e = ex;
            if (checkInner)
                e = e.GetBaseException();

            ErrorMessage = e.Message;
        }
    </s:String>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=busObject/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=busObject/Expression/@EntryValue">complete()</s:String>
    <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=busObject/Order/@EntryValue">0</s:Int64>
    <s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=NewLiteral/@KeyIndexDefined">True</s:Boolean>
    <s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=NewLiteral/Expression/@EntryValue">complete()</s:String>
    <s:Int64 x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=E88A906D39C741C0A3B8095C5063DADE/Field/=NewLiteral/Order/@EntryValue">1</s:Int64>
</root>    

使用这种疯狂的格式,无法分辨一组代码片段的开始和结束的位置。每个代码片段都有多个 Key,加上 GUID 标识,这使得匹配现有的代码段来判断是否存在的目的几乎不可能实现。

据我所知,没有找到任何相关键值配置的文档,也没有如何存储的文档。很有可能存在其他存储选项,但看起来 Rider 并没有为代码片段设置编辑功能。如果您有更好的开发人员文档,请发表评论。

出于这个原因,Rider 导入是一次性的,如果您导出两次相同的片段,它们就会翻倍。

为了测试,我在 Rider 的导出文件中添加了一个标记键。然后,在我导入相同的代码片段时,我会删除了之前添加的代码片段。很简陋,也只是测试阶段。如果相关的配置发生了变化,则可能会失效。

此格式仅适用于 Rider 支持的 .NET 特定代码类型:.NET Languages,Razor 和包含 HTML 模板的 WebForms。其他格式( JavaScript、HTML 、CSS)则使用完全独立的格式,我没有精力在实现相关的功能。对于 Rider,我主要关心的是 C# 和 HTML 模板,能正常运行就好了。

只需导出特定文件夹,如 C# 文件夹或 HTML 代码段,而不是批量导出整个代码片段文件夹。

SnippetConverter "~2017\Visual C#\My Code Snippets" -m vs-rider -d
SnippetConverter "~2017\Code Snippets\Visual Web Developer\My HTML Snippets" -m vs-rider -d

摘要

正如我前面提到的,所有这些都是非常简陋,但对于将我全部的代码片段从 Visual Studio 导出到 Visual Studio Code 是完全够用的。对于 Rider, C# 和 HTML 代码片段导出也可以做到,但是其他类型(如 JavaScript、CSS)会出现异常。我只是当作个人工具,如果哪天有足够的兴趣的话,我会接着完善,但是很大程度是需要另外搞一个完全独立的转换器。

我没有测试所有的 Visual Studio 支持的文件类型,即使是VS 内置的代码片段也可能存在某些问题。保险一点,请不要批量导出所有代码段,而是单独导出每种类型的代码片段。

我还是强烈建议使用前缀,因为可以更容易地找到你的代码片段,并保持它们不受影响。

现在这个工具对于我来说已经足够了,但是我很想知道我是否是少数几个投身到代码片段转换的人之一😃

相关资源

posted @ 2019-01-18 21:42 张蘅水 阅读(...) 评论(...) 编辑 收藏