Program.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using System;
  2. using System.IO;
  3. using System.Runtime.InteropServices;
  4. using System.Runtime.InteropServices.ComTypes;
  5. using System.Text.RegularExpressions;
  6. using EnvDTE;
  7. namespace GodotTools.OpenVisualStudio
  8. {
  9. internal static class Program
  10. {
  11. [DllImport("ole32.dll")]
  12. private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable pprot);
  13. [DllImport("ole32.dll")]
  14. private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
  15. [DllImport("user32.dll")]
  16. private static extern bool SetForegroundWindow(IntPtr hWnd);
  17. private static void ShowHelp()
  18. {
  19. Console.WriteLine("Opens the file(s) in a Visual Studio instance that is editing the specified solution.");
  20. Console.WriteLine("If an existing instance for the solution is not found, a new one is created.");
  21. Console.WriteLine();
  22. Console.WriteLine("Usage:");
  23. Console.WriteLine(@" GodotTools.OpenVisualStudio.exe solution [file[;line[;col]]...]");
  24. Console.WriteLine();
  25. Console.WriteLine("Lines and columns begin at one. Zero or lower will result in an error.");
  26. Console.WriteLine("If a line is specified but a column is not, the line is selected in the text editor.");
  27. }
  28. // STAThread needed, otherwise CoRegisterMessageFilter may return CO_E_NOT_SUPPORTED.
  29. [STAThread]
  30. private static int Main(string[] args)
  31. {
  32. if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
  33. {
  34. ShowHelp();
  35. return 0;
  36. }
  37. string solutionFile = NormalizePath(args[0]);
  38. var dte = FindInstanceEditingSolution(solutionFile);
  39. if (dte == null)
  40. {
  41. // Open a new instance
  42. dte = TryVisualStudioLaunch("VisualStudio.DTE.17.0");
  43. if (dte == null)
  44. {
  45. // Launch of VS 2022 failed, fallback to 2019
  46. dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0");
  47. }
  48. dte.UserControl = true;
  49. try
  50. {
  51. dte.Solution.Open(solutionFile);
  52. }
  53. catch (ArgumentException)
  54. {
  55. Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
  56. return 1;
  57. }
  58. dte.MainWindow.Visible = true;
  59. }
  60. MessageFilter.Register();
  61. try
  62. {
  63. // Open files
  64. for (int i = 1; i < args.Length; i++)
  65. {
  66. // Both the line number and the column begin at one
  67. string[] fileArgumentParts = args[i].Split(';');
  68. string filePath = NormalizePath(fileArgumentParts[0]);
  69. try
  70. {
  71. dte.ItemOperations.OpenFile(filePath);
  72. }
  73. catch (ArgumentException)
  74. {
  75. Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
  76. return 1;
  77. }
  78. if (fileArgumentParts.Length > 1)
  79. {
  80. if (int.TryParse(fileArgumentParts[1], out int line))
  81. {
  82. var textSelection = (TextSelection)dte.ActiveDocument.Selection;
  83. if (fileArgumentParts.Length > 2)
  84. {
  85. if (int.TryParse(fileArgumentParts[2], out int column))
  86. {
  87. textSelection.MoveToLineAndOffset(line, column);
  88. }
  89. else
  90. {
  91. Console.Error.WriteLine("The column part of the argument must be a valid integer");
  92. return 1;
  93. }
  94. }
  95. else
  96. {
  97. textSelection.GotoLine(line, Select: true);
  98. }
  99. }
  100. else
  101. {
  102. Console.Error.WriteLine("The line part of the argument must be a valid integer");
  103. return 1;
  104. }
  105. }
  106. }
  107. }
  108. finally
  109. {
  110. var mainWindow = dte.MainWindow;
  111. mainWindow.Activate();
  112. SetForegroundWindow(new IntPtr(mainWindow.HWnd));
  113. MessageFilter.Revoke();
  114. }
  115. return 0;
  116. }
  117. private static DTE TryVisualStudioLaunch(string version)
  118. {
  119. try
  120. {
  121. var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
  122. var dte = (DTE)Activator.CreateInstance(visualStudioDteType);
  123. return dte;
  124. }
  125. catch (COMException)
  126. {
  127. return null;
  128. }
  129. }
  130. private static DTE FindInstanceEditingSolution(string solutionPath)
  131. {
  132. if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
  133. return null;
  134. try
  135. {
  136. pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
  137. ppenumMoniker.Reset();
  138. var moniker = new IMoniker[1];
  139. while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
  140. {
  141. string ppszDisplayName;
  142. CreateBindCtx(0, out IBindCtx ppbc);
  143. try
  144. {
  145. moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
  146. }
  147. finally
  148. {
  149. Marshal.ReleaseComObject(ppbc);
  150. }
  151. if (ppszDisplayName == null)
  152. continue;
  153. // The digits after the colon are the process ID
  154. if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.16.0:[0-9]"))
  155. continue;
  156. if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
  157. {
  158. if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
  159. {
  160. if (NormalizePath(dte.Solution.FullName) == solutionPath)
  161. return dte;
  162. }
  163. }
  164. }
  165. }
  166. finally
  167. {
  168. Marshal.ReleaseComObject(pprot);
  169. }
  170. return null;
  171. }
  172. static string NormalizePath(string path)
  173. {
  174. return new Uri(Path.GetFullPath(path)).LocalPath
  175. .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
  176. .ToUpperInvariant();
  177. }
  178. #region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
  179. private class MessageFilter : IOleMessageFilter
  180. {
  181. // Class containing the IOleMessageFilter
  182. // thread error-handling functions
  183. private static IOleMessageFilter _oldFilter;
  184. // Start the filter
  185. public static void Register()
  186. {
  187. IOleMessageFilter newFilter = new MessageFilter();
  188. int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
  189. if (ret != 0)
  190. Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
  191. }
  192. // Done with the filter, close it
  193. public static void Revoke()
  194. {
  195. int ret = CoRegisterMessageFilter(_oldFilter, out _);
  196. if (ret != 0)
  197. Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
  198. }
  199. //
  200. // IOleMessageFilter functions
  201. // Handle incoming thread requests
  202. int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
  203. {
  204. // Return the flag SERVERCALL_ISHANDLED
  205. return 0;
  206. }
  207. // Thread call was rejected, so try again.
  208. int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
  209. {
  210. if (dwRejectType == 2)
  211. // flag = SERVERCALL_RETRYLATER
  212. {
  213. // Retry the thread call immediately if return >= 0 & < 100
  214. return 99;
  215. }
  216. // Too busy; cancel call
  217. return -1;
  218. }
  219. int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
  220. {
  221. // Return the flag PENDINGMSG_WAITDEFPROCESS
  222. return 2;
  223. }
  224. // Implement the IOleMessageFilter interface
  225. [DllImport("ole32.dll")]
  226. private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
  227. }
  228. [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  229. private interface IOleMessageFilter
  230. {
  231. [PreserveSig]
  232. int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
  233. [PreserveSig]
  234. int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
  235. [PreserveSig]
  236. int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
  237. }
  238. #endregion
  239. }
  240. }