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