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