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

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

导语

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


译文:

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

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

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

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

所以,在过去的几个周末,我作了一个将 Visual Studio 中的代码片断导出到 VS Code 中的小工具,同时尽可能能导出到 JetBrains Rider 。编程

若是你感兴趣,能够在GitHub上找到代码:json

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

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

代码片断转换器

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 内置的代码片断也可能存在某些问题。保险一点,请不要批量导出全部代码段,而是单独导出每种类型的代码片断。

我仍是强烈建议使用前缀,由于能够更容易地找到你的代码片断,并保持它们不受影响。

如今这个工具对于我来讲已经足够了,可是我很想知道我是不是少数几个投身到代码片断转换的人之一😃

相关资源

相关文章
相关标签/搜索