Program.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. if (dte == null)
  48. {
  49. Console.Error.WriteLine("Visual Studio not found");
  50. return 1;
  51. }
  52. }
  53. dte.UserControl = true;
  54. try
  55. {
  56. dte.Solution.Open(solutionFile);
  57. }
  58. catch (ArgumentException)
  59. {
  60. Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
  61. return 1;
  62. }
  63. dte.MainWindow.Visible = true;
  64. }
  65. MessageFilter.Register();
  66. try
  67. {
  68. // Open files
  69. for (int i = 1; i < args.Length; i++)
  70. {
  71. // Both the line number and the column begin at one
  72. string[] fileArgumentParts = args[i].Split(';');
  73. string filePath = NormalizePath(fileArgumentParts[0]);
  74. try
  75. {
  76. dte.ItemOperations.OpenFile(filePath);
  77. }
  78. catch (ArgumentException)
  79. {
  80. Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
  81. return 1;
  82. }
  83. if (fileArgumentParts.Length > 1)
  84. {
  85. if (int.TryParse(fileArgumentParts[1], out int line))
  86. {
  87. var textSelection = (TextSelection)dte.ActiveDocument.Selection;
  88. if (fileArgumentParts.Length > 2)
  89. {
  90. if (int.TryParse(fileArgumentParts[2], out int column))
  91. {
  92. textSelection.MoveToLineAndOffset(line, column);
  93. }
  94. else
  95. {
  96. Console.Error.WriteLine("The column part of the argument must be a valid integer");
  97. return 1;
  98. }
  99. }
  100. else
  101. {
  102. textSelection.GotoLine(line, Select: true);
  103. }
  104. }
  105. else
  106. {
  107. Console.Error.WriteLine("The line part of the argument must be a valid integer");
  108. return 1;
  109. }
  110. }
  111. }
  112. }
  113. finally
  114. {
  115. var mainWindow = dte.MainWindow;
  116. mainWindow.Activate();
  117. SetForegroundWindow(mainWindow.HWnd);
  118. MessageFilter.Revoke();
  119. }
  120. return 0;
  121. }
  122. private static DTE? TryVisualStudioLaunch(string version)
  123. {
  124. try
  125. {
  126. var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
  127. var dte = (DTE?)Activator.CreateInstance(visualStudioDteType!);
  128. return dte;
  129. }
  130. catch (COMException)
  131. {
  132. return null;
  133. }
  134. }
  135. private static DTE? FindInstanceEditingSolution(string solutionPath)
  136. {
  137. if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
  138. return null;
  139. try
  140. {
  141. pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
  142. ppenumMoniker.Reset();
  143. var moniker = new IMoniker[1];
  144. while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
  145. {
  146. string ppszDisplayName;
  147. CreateBindCtx(0, out IBindCtx ppbc);
  148. try
  149. {
  150. moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
  151. }
  152. finally
  153. {
  154. Marshal.ReleaseComObject(ppbc);
  155. }
  156. if (ppszDisplayName == null)
  157. continue;
  158. // The digits after the colon are the process ID
  159. if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.1[6-7].0:[0-9]"))
  160. continue;
  161. if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
  162. {
  163. if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
  164. {
  165. if (NormalizePath(dte.Solution.FullName) == solutionPath)
  166. return dte;
  167. }
  168. }
  169. }
  170. }
  171. finally
  172. {
  173. Marshal.ReleaseComObject(pprot);
  174. }
  175. return null;
  176. }
  177. private static string NormalizePath(string path)
  178. {
  179. return new Uri(Path.GetFullPath(path)).LocalPath
  180. .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
  181. .ToUpperInvariant();
  182. }
  183. #region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
  184. private class MessageFilter : IOleMessageFilter
  185. {
  186. // Class containing the IOleMessageFilter
  187. // thread error-handling functions
  188. private static IOleMessageFilter? _oldFilter;
  189. // Start the filter
  190. public static void Register()
  191. {
  192. IOleMessageFilter newFilter = new MessageFilter();
  193. int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
  194. if (ret != 0)
  195. Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
  196. }
  197. // Done with the filter, close it
  198. public static void Revoke()
  199. {
  200. int ret = CoRegisterMessageFilter(_oldFilter, out _);
  201. if (ret != 0)
  202. Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
  203. }
  204. //
  205. // IOleMessageFilter functions
  206. // Handle incoming thread requests
  207. int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
  208. {
  209. // Return the flag SERVERCALL_ISHANDLED
  210. return 0;
  211. }
  212. // Thread call was rejected, so try again.
  213. int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
  214. {
  215. // flag = SERVERCALL_RETRYLATER
  216. if (dwRejectType == 2)
  217. {
  218. // Retry the thread call immediately if return >= 0 & < 100
  219. return 99;
  220. }
  221. // Too busy; cancel call
  222. return -1;
  223. }
  224. int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
  225. {
  226. // Return the flag PENDINGMSG_WAITDEFPROCESS
  227. return 2;
  228. }
  229. // Implement the IOleMessageFilter interface
  230. [DllImport("ole32.dll")]
  231. private static extern int CoRegisterMessageFilter(IOleMessageFilter? newFilter, out IOleMessageFilter? oldFilter);
  232. }
  233. [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  234. private interface IOleMessageFilter
  235. {
  236. [PreserveSig]
  237. int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
  238. [PreserveSig]
  239. int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
  240. [PreserveSig]
  241. int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
  242. }
  243. #endregion
  244. }
  245. }