dot netconf2017 - vs拡張
TRANSCRIPT
Dawn Huczek
Quick shot 作成時に学んだVS拡張、Roslyn、Dotnet.exe関連の知識
自己紹介
・石川達也
・(株)Codeer 代表取締役
・Microsoft MVP
・ささいなことですが(ブログ)
・OSS
FriendlySelenium拡張
LambdicSql
Visual Studio and Development Technologies
http://ishikawa-tatsuya.hatenablog.com/
https://www.nuget.org/profiles/ishikawa-tatsuya
趣味はギターとライブラリ作成
Codeer Ltd.
こんなメンバーでやってます!
ソフトウェア開発でお悩みの方は、いつでもご相談ください
Quick shot
関数単体で実行するVS拡張(無料)
Quick shot デモ
https://marketplace.visualstudio.com/items?itemName=ishikawa-tatsuya.Quickshot
λ sql
仲間募集中!
LambdicSqlデモ
https://github.com/Codeer-Software/LambdicSql
SQL Server LambdicSql.SqlServer
My Sql
SQLite
Oracle
DB2
PostgreSql
95%
実装状況
共通 LabmdicSql 99%
LambdicSql.MySql
LambdicSql.SQLite
LambdicSql.Oracle
LambdicSql.DB2
LambdicSql.Npgsql
もっと仲間が欲しいのです!
戦いは数だよ兄貴!
もし わしの味方になれば世界(実装)の半分を わけてやろう・・・
この世の全てのSQLをC#で表現してやる!
仲間募集中!
VS拡張の話
ようやく・・・
Quick shot を作るには?
・右クリックメニューを表示・ドッキングウィンドウを表示・VS2015にも対応・ソリューションビルド・選択位置の関数の情報を取得・追加コードのコンパイル・関数実行
新規にVSIXプロジェクトを作成する
Visual Studio 2017 で VSIXプロジェクトを作成する
作成直後
ビルドして実行すると拡張をデバッグする用のVSが起動する
右クリックメニュー
右クリメニュー用のコマンド追加
ビルド→起動
デフォルトではツールメニュー以下で表示される
押すとメッセージが出る
RClickCommand.csにサンプルコードが書かれていて、そこにメッセージを表示するコードがある
Vsctファイル その前に名前を変更する(任意)
・そもそも、最初に作ったコマンド名がファイル名になっているのでファイル名を変更する。
RClickCommandPackage.vsct→ExTest.vsctRClickCommandPakage.cs → ExtTestPackage.cs
・見通しが悪いのでコメントをすべて消す。
→
・識別子の名前も変更guidRClickCommandPackage → guidTestExPackageguidRClickCommandPackageCmdSet → guidExTestCmdSetMyMenuGroup → CodeEditorGroup
<Groups><Group guid="guidExTestCmdSet" id="CodeEditorGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/></Group>
</Groups>
Vsctファイル エディタ上で右クリックで表示されるようにする
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
https://msdn.microsoft.com/ja-jp/library/microsoft.visualstudio.shell.vsmenus.aspx
C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VSSDK\VisualStudioIntegration\Common\Inc\vsshlids.h
vsshlids.hに定義されている IDM_VS_ で始まる識別子をいくつか試してみる
ソースコードの右クリックメニュー以外で表示したい場合は・・・
<Extern href="stdidcmd.h"/><Extern href="vsshlids.h"/>
vsctファイルの先頭でインクルードされている
Vsctファイル ついでにショートカットキーもつける
<KeyBindings><KeyBinding guid="guidExTestCmdSet" id="RClickCommandId“editor="guidVSStd97" key1="Z" mod1="Control Shift" />
</KeyBindings>
https://msdn.microsoft.com/ja-jp/library/cc138531.aspx
guidVSStd97が何を意味するのかは知らん
これを入れる
</CommandTable> ←これが閉じる前くらいに入れる
Vsctファイル ついでにアイコンも変える
<Icon guid="guidImages" id="bmpPic1" />
<Bitmaps><Bitmap guid="guidImages" href="Resources\RClickCommand.png" usedList="bmpPic1, …"/>
</Bitmaps>
って書いてるので
RClickCommand.pngの最初の16×16に好きな絵を張り付ける
こんな感じになります
ドッキングウィンドウ
ドッキングウィンドウ追加
追加はできるが・・・
ビルド・・・
おい!
Image?
ビルド・・・
修正・・・
←削除
ExTest.vsctも変更する
新たに追加されたImage系の定義を削除してRClickCommandと同じものを使うようにする
どうやら、生成された
MyDockingWindowCommand.pngがゴミっている。Paintとかでも開けない。
Image差し替えてもいいよ
(気を取り直して)ビルド→起動
表示→その他ウィンドウ→MyDockingWindow
デフォルトではフローティングで表示される
アウトプットにドッキング表示されるようにする
[ProvideToolWindow(typeof(MyDockingWindow), Style = VsDockStyle.Tabbed, Window = ToolWindowGuids.Outputwindow)]public sealed class ExtTestPackage : Package
ExtTestPackageの属性に赤字のコードを足す
ここで一旦、VS2015対応
source.extension.vsixmanifest
ギャラリーにUpしたときに、この情報を使ってアイコンとか表示してくれる。リリースの時にVersionを上げておくとダウンロードした人が更新インストールできる(上げてないと、一回アンインストールが必要だし、そもそもバージョンアップされたかわからない
source.extension.vsixmanifest 2015対応
14(2015)から対応していて16(201x)には対応していませんって意味らしい
[14.0, 16.0)
source.extension.vsixmanifest 2015対応
source.extension.vsixmanifest 2015対応
Visual Studio MPF 15.0 を消す
これで終わりかと思いきや・・・
参照関係を修正する VS2015対応
←赤枠の参照を消す
↧全部Nugetで消します依存関係がある。左のツリーの上から消すと上手く消える
代わりに
Microsoft.VisualStudio.Shell.14.0を入れる
参照関係を修正する VS2015対応
VS2015にインストールしてみる
2015対応大変やった・・・
実装編
Visual Stuidoの機能にアクセスする
DTEを使ってVS自体を操作します。色々できるようですが、この資料では
・Solution・ActiveDocument・Debugger
に触れてみます。その他のもDTE2の定義を見て勘で使ってみてください。
ググり力が重要・・・
var dte = Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE2;
プロジェクトをビルドする
dte.Solution. SolutionBuildが使えそうだなー
public interface SolutionBuild{void BuildProject(string SolutionConfiguration,
string ProjectUniqueName,bool WaitForBuildToFinish = false);
いやいや、SolutionConfigrationって何入れたらええねん・・・
var doc = dte.ActiveDocument;if (doc == null) return false;
//選択されているビルドのモード(Debug|AnyCpu)var solutionBuild = DTE.Solution.SolutionBuild;var buildConfig = solutionBuild.ActiveConfiguration.Name;
//選択されているプロジェクト名称var proUniquName = doc.ProjectItem.ContainingProject.UniqueName;
//選択されているプラットフォームvar platform = solutionBuild.ActiveConfiguration.
SolutionContexts.Cast<SolutionContext>().FirstOrDefault().PlatformName;
if (platform == "Win32") platform = "x86";
//ビルドsolutionBuild.BuildProject(buildConfig + "|" + platform, proUniquName, false);
試行錯誤の末、何とかたどりつく・・・
なんかダメ!
ビルドできるんだけど、複数プロジェクトがあるときにアウトプットに変なログが出る(手元にログ残してない。ごめん
どうすりゃいいんだよ!
ググったら出てくる
dte.ExecuteCommand("Build.BuildSelection", "");
なんじゃそれ・・・
//ビルド > ソリューションのビルドならばDTE.ExecuteCommand("Build.BuildSolution");
//ビルド > ソリューションのリビルドならばDTE.ExecuteCommand("Build.RebuildSolution");
//ビルド > xxxのビルドDTE.ExecuteCommand("Build.BuildSelection");
//ビルド > xxxのリビルドDTE.ExecuteCommand("Build.RebuildSelection");
http://microsoft.public.jp.dotnet.languages.vc.narkive.com/pukiYoAo
ちなみに、こうらしい
public interface _Solution : IEnumerable{
Project Item(object index);IEnumerator GetEnumerator();void SaveAs(string FileName);Project AddFromTemplate(string FileName, string Destination, string ProjectName, bool Exclusive = false);Project AddFromFile(string FileName, bool Exclusive = false);void Open(string FileName);void Close(bool SaveFirst = false);void Remove(Project proj);string get_TemplatePath(string ProjectType);object get_Extender(string ExtenderName);void Create(string Destination, string Name);ProjectItem FindProjectItem(string FileName);string ProjectItemsTemplatePath(string ProjectKind);DTE DTE { get; }DTE Parent { get; }int Count { get; }string FileName { get; }Properties Properties { get; }bool IsDirty { get; set; }string FullName { get; }bool Saved { get; set; }Globals Globals { get; }AddIns AddIns { get; }object ExtenderNames { get; }string ExtenderCATID { get; }bool IsOpen { get; }SolutionBuild SolutionBuild { get; }Projects Projects { get; }
}
Solution
ビルドではイマイチやったけどプロジェクトの一覧取ったりソリューション全体を操作できて便利な子ではあります。
右クリックした位置にある関数情報の取得
クリックした位置を取得 dte.ActiveDocument
var csFile = Path.Combine(dte.ActiveDocument.Path, dte.ActiveDocument.Name);
//ファイルを読むvar csLines = File.ReadAllLines(csFile);
//選択位置取得var sel = dte.ActiveDocument.Selection as TextSelection;var pos = GetPos(csLines, sel.CurrentLine - 1, sel.CurrentColumn - 1);
static int GetPos(string[] lines, int currentLine, int currentCol){
int charCount = 0;for (int i = 0; i < lines.Length; i++){
if (i == currentLine){
return charCount + currentCol;}charCount += lines[i].Length;charCount += Environment.NewLine.Length;
}return -1;
}
dte.ActiveDocumentを使う
Roslyn
Microsoft.CodeAnalysis
//Roslynで解析var tree = CSharpSyntaxTree.ParseText(string.Join(Environment.NewLine, csLines));
//選択クラスvar classSyntaxs = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().
//含まれているWhere(x => x.Span.Contains(pos));
//ネームスペースvar namespaceSyntax = tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>().
//先頭FirstOrDefault();
//選択メソッドvar methodSyntax = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().
//含まれているWhere(x => x.Span.Contains(pos)).//先頭FirstOrDefault();
選択位置の関数の型情報的なものを取得 Roslyn
もっといい方法があるかも
//ネームスペースvar namespaceName = namespaceSyntax.Name.GetText().ToString().Trim();
//クラスvar className = string.Join("+", classSyntaxs.Select(x => x.Identifier.Text.Trim()));
//型完全名var typeFullName = namespaceName + "." + className;
//関数名var methodName = methodSyntax.Identifier.Text.Trim();
//戻り値(不完全な情報)var returnType = methodSyntax.ReturnType.ToFullString().Trim();
//引数(不完全な情報)var argumentTypes = new List<string>();var argumentNames = new List<string>();foreach (var e in methodSyntax.ParameterList.Parameters){
argumentTypes.Add(e.Type.ToFullString().Trim());argumentNames.Add(e.Identifier.Text.Trim());
}
選択位置の関数の型情報的なものを取得 Roslyn
ちなみに、2015だと(ていうかVSの持っているRoslynと)同一AppDomainに複数のRoslynのdllがロードされるけど大丈夫みたい。
へー
型情報を得るMono.Cecil
ここでは普通のリフレクションは使いづらい
型情報を得る
アセンブリのパスを取得
もっといい方法があるかも
var proj = dte.ActiveDocument.ProjectItem.ContainingProject.FileName;var solutionBuild = dte.Solution.SolutionBuild;
//選択されているビルドのモード(Debug)var buildConfig = solutionBuild.ActiveConfiguration.Name;
//選択されているプラットフォームvar platform = solutionBuild.ActiveConfiguration.
SolutionContexts.Cast<SolutionContext>().FirstOrDefault().PlatformName;
//CSファイル解析は端折ります・・・ XML解析です。
//出力フォルダ取得var output = CSProjAnalyzer.GetOutputDirectory(proj, buildConfig, platform);
//拡張子var ext = CSProjAnalyzer.GetTargetFileExtension(proj);
//バイナリパスvar assembly = Path.Combine(output, CSProjAnalyzer.GetAssemblyName(proj) + "." + ext);
型情報を得る Mono.Cecil
//アセンブリ情報取得var asm = AssemblyDefinition.ReadAssembly(assemblyPath);
//タイプvar type = asm.Modules.SelectMany(e => e.Types).
Where(e => e.FullName == typeFullName).FirstOrDefault();
//完全なマッチロジックにはならないが、実用上問題ないレベルではある・・・var methodInfo = type.Methods.Where(e => e.Name == methodName).
Where(e => IsMatchMethod(e, methodName, argumentTypes)).FirstOrDefault();
割り切った
private static bool IsMatchMethod(MethodDefinition methodInfo, string methodName, List<string> argumentTypes){
if (methodInfo.Name != methodName) return false;if (methodInfo.Parameters.Count != argumentTypes.Count) return false;for (int i = 0; i < methodInfo.Parameters.Count; i++){
if (!IsMatchType(methodInfo.Parameters[i].ParameterType, argumentTypes[i])) return false;}return true;
}
static bool IsMatchType(TypeReference type, string typeName){
var types1 = GetAllTypes(type);var types2 = GetAllTypes(typeName);if (types1.Length != types2.Length) return false;for (int i = 0; i < types1.Length; i++){
if (!types1[i].Contains(types2[i])) return false;}return true;
}
static string[] GetAllTypes(string type){
return type.Replace(">", "").Split(new[] { "<", "," }, System.StringSplitOptions.RemoveEmptyEntries).Select(e => AdjustTypeName(e)).ToArray();}
static string[] GetAllTypes(TypeReference type){
var types = new List<string>();if (type.IsGenericInstance){
types.AddRange(GetAllTypes(type.GetElementType()));foreach (var e in ((dynamic)type).GenericArguments){
types.AddRange(GetAllTypes(e));}
}else{
//Innerクラスの表現が違うので合わせるtypes.Add(type.FullName.Replace("/", "+"));
}return types.ToArray();
}
static string AdjustTypeName(string type){
type = type.Trim();var arrayCount = GetArrayCount(type);type = type.Replace("[]", string.Empty);switch (type){
case "byte": type = typeof(byte).FullName; break;case "char": type = typeof(char).FullName; break;case "short": type = typeof(short).FullName; break;case "ushort": type = typeof(ushort).FullName; break;case "int": type = typeof(int).FullName; break;case "uint": type = typeof(uint).FullName; break;case "long": type = typeof(long).FullName; break;case "ulong": type = typeof(ulong).FullName; break;case "float": type = typeof(float).FullName; break;case "double": type = typeof(double).FullName; break;case "decimal": type = typeof(decimal).FullName; break;case "string": type = typeof(string).FullName; break;
}return type + string.Join("", Enumerable.Range(0, arrayCount).Select(e => "[]"));
}
static int GetArrayCount(string type){
int count = 0;while (true){
int index = type.IndexOf("[]");if (index == -1) break;count++;type = type.Substring(index + "[]".Length);
}return count;
}
IsMatchMethodは関数名と引数から、(だいたい)マッチしてるか判別するロジックです。
実行 FullDotNet編
VS
Host.exe起動(ファイルで情報を渡す)
コンパイル
リフレクションで実行
デバッグなら待ち
結果取得(ファイル)
構成
Host.exeはリソースに仕込みました。
実行前に、存在しないかバイナリが異なればコピーする
C:¥ProgramData¥QuickShot
リフレクションはアセンブリの解決が必要
C:\ProgramData\QuickShot
QuickShot.ExecutionHost.exe
対象プロジェクトのDebug or Releaseフォルダ
対象のdll群
ここにあるもの以外は解決できない
class AssemblyResolver{
static string[] _dllDirectories;
internal static void Init(string[] dllDirectories){
_dllDirectories = dllDirectories;AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;
}
static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args){
var sep = args.Name.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);foreach (var e in _dllDirectories){
var path = Path.Combine(e, sep[0] + ".dll");if (File.Exists(path)){
return Assembly.LoadFrom(path);}path = Path.Combine(e, sep[0] + ".exe");if (File.Exists(path)){
return Assembly.LoadFrom(path);}
}return null;
}}
AppDomain.CurrentDomain.AssemblyResolve を使います
やったことないけどプロービング設定でも行けるかも
コンパイルはRoslynを内部的に使ったCodeDom
最初から素直にRoslynにしておけば良かった・・・
https://opcdiary.net/?p=32908
CSharpCodeProvider codeProvider = new Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider();
Microsoft.CodeDom.Providers.DotNetCompilerPlatform
Roslyn/Binフォルダが必要です。
Asp.Netのプロジェクト作るとできているのでそれをコピります
Bin/Release/Roslyn/Binとかに置く今回はこれもResourcesに入れてProgramData以下にコピった。
最初から素直にRoslynにしておけば良かった・・・
デバッグはこんな感じで実現しました。
Host.exe
VS
if (info.IsAttach){
while (!System.Dianostics.Debugger.IsAttached){
Thread.Sleep(50);}
}
EnvDTE.Processes processes = DTE.Debugger.LocalProcesses;foreach (EnvDTE.Process proc in processes){
if (proc.ProcessID == hostProcessId){
proc.Attach();return;
}}
実行 DotNetCore、DotNetStandard編
リフレクションが上手くいかん!
なんでこんな仕様にしたんだよ・・・
deps.jsonってのがあってそこにないものはロードできない・・・
リフレクションだけでなんとかできる気がしない・・・
・Deps.jsonを書き換えるのはしんどい
・そもそもdllが分散配置されてて解決するもの大変(binフォルダにコピられてない)
方針
Host.exeは対象のプロジェクトごとにコードを作成、コンパイルする
でも、できるだけキャッシュするようにしたんですよ・・・
csproj作成
ファイルは同一フォルダにいれたらOK
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>
<OutputType>Exe</OutputType><TargetFramework>netcoreapp1.1</TargetFramework><ApplicationIcon /><StartupObject />
</PropertyGroup>
<ItemGroup><PackageReference Include="Newtonsoft.Json" Version="10.0.3" /><PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<ItemGroup><ProjectReference Include = "c:\work\StandardTest\StandardTest.csproj" />
</ItemGroup></Project>
DotNetCoreのcsprojはシンプル
↑対象のに合わせる
↧host.exeで使う参照
↧対象プロジェクトへのフルパス
ビルド、実行はdotnetexeを使う
あれ?今日のチャックさんの話きいたらRunだけでよかったんじゃ・・・
コマンドラインの情報
https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet?tabs=netcore2x
dotnet restoredotnet builddotnet 対象.dll
今回使ったのは以下
Restore 実行
WorkDirの設定がポイント
var exeOutput = new List<string>(); var p = Process.Start(new ProcessStartInfo{
FileName = "dotnet",Arguments = "restore",WorkingDirectory = workDir,UseShellExecute = false,CreateNoWindow = true,RedirectStandardOutput = true
});p.OutputDataReceived += (_, e) =>{
exeOutput.Add(e.Data);};p.BeginOutputReadLine();p.WaitForExit();
Builld実行
出力も取っておく
var p = Process.Start(new ProcessStartInfo{
FileName = "dotnet",Arguments = "build" + (isDebug ? string.Empty : " -c Release"),WorkingDirectory = workDir,UseShellExecute = false,CreateNoWindow = true,RedirectStandardOutput = true
});p.OutputDataReceived += (_, e) =>{
exeOutput.Add(e.Data);};p.BeginOutputReadLine();p.WaitForExit();
処理実行
他のもそうだけど、待ちは非同期で実行してメインスレッドを固めないようにしました。デバッグもできなくなるしね
var p = Process.Start(new ProcessStartInfo{
FileName = "dotnet",Arguments = isAttach ? "QuickShotExecuter.dll -a" : "QuickShotExecuter.dll",WorkingDirectory = targetDllDir,UseShellExecute = false,CreateNoWindow = true,
}); p.WaitForExit();
QuickShotのTODO
・DotNetCore、DotNetStandardをもう少し速くしたい
・Xamarinとかも対応できないかなー
最後になりましたが、QuickShotのInternal なオブジェクトを生成する部分の実装はneue.ccさんの ChainingAssertionのDynamicAccessorの実装大幅にパクりました。
あざっす!
謝辞
・Friendlyhttps://github.com/Codeer-Software/Friendly.Windows
・LambdicSqlhttps://github.com/Codeer-Software/LambdicSql
・QuickShothttps://marketplace.visualstudio.com/items?itemName=ishikawa-tatsuya.Quickshot
みんな使ってね!
ご清聴ありがとうございました!