|
@@ -0,0 +1,270 @@
|
|
|
+using System;
|
|
|
+using System.IO;
|
|
|
+using System.Runtime.InteropServices;
|
|
|
+using System.Runtime.InteropServices.ComTypes;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+using EnvDTE;
|
|
|
+
|
|
|
+namespace GodotTools.OpenVisualStudio
|
|
|
+{
|
|
|
+ internal static class Program
|
|
|
+ {
|
|
|
+ [DllImport("ole32.dll")]
|
|
|
+ private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable pprot);
|
|
|
+
|
|
|
+ [DllImport("ole32.dll")]
|
|
|
+ private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
|
|
|
+
|
|
|
+ [DllImport("user32.dll")]
|
|
|
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
|
+
|
|
|
+ private static void ShowHelp()
|
|
|
+ {
|
|
|
+ Console.WriteLine("Opens the file(s) in a Visual Studio instance that is editing the specified solution.");
|
|
|
+ Console.WriteLine("If an existing instance for the solution is not found, a new one is created.");
|
|
|
+ Console.WriteLine();
|
|
|
+ Console.WriteLine("Usage:");
|
|
|
+ Console.WriteLine(@" GodotTools.OpenVisualStudio.exe solution [file[;line[;col]]...]");
|
|
|
+ Console.WriteLine();
|
|
|
+ Console.WriteLine("Lines and columns begin at one. Zero or lower will result in an error.");
|
|
|
+ Console.WriteLine("If a line is specified but a column is not, the line is selected in the text editor.");
|
|
|
+ }
|
|
|
+
|
|
|
+ // STAThread needed, otherwise CoRegisterMessageFilter may return CO_E_NOT_SUPPORTED.
|
|
|
+ [STAThread]
|
|
|
+ private static int Main(string[] args)
|
|
|
+ {
|
|
|
+ if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
|
|
|
+ {
|
|
|
+ ShowHelp();
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ string solutionFile = NormalizePath(args[0]);
|
|
|
+
|
|
|
+ var dte = FindInstanceEditingSolution(solutionFile);
|
|
|
+
|
|
|
+ if (dte == null)
|
|
|
+ {
|
|
|
+ // Open a new instance
|
|
|
+
|
|
|
+ var visualStudioDteType = Type.GetTypeFromProgID("VisualStudio.DTE.16.0", throwOnError: true);
|
|
|
+ dte = (DTE)Activator.CreateInstance(visualStudioDteType);
|
|
|
+
|
|
|
+ dte.UserControl = true;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ dte.Solution.Open(solutionFile);
|
|
|
+ }
|
|
|
+ catch (ArgumentException)
|
|
|
+ {
|
|
|
+ Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ dte.MainWindow.Visible = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ MessageFilter.Register();
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // Open files
|
|
|
+
|
|
|
+ for (int i = 1; i < args.Length; i++)
|
|
|
+ {
|
|
|
+ // Both the line number and the column begin at one
|
|
|
+
|
|
|
+ string[] fileArgumentParts = args[i].Split(';');
|
|
|
+
|
|
|
+ string filePath = NormalizePath(fileArgumentParts[0]);
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ dte.ItemOperations.OpenFile(filePath);
|
|
|
+ }
|
|
|
+ catch (ArgumentException)
|
|
|
+ {
|
|
|
+ Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fileArgumentParts.Length > 1)
|
|
|
+ {
|
|
|
+ if (int.TryParse(fileArgumentParts[1], out int line))
|
|
|
+ {
|
|
|
+ var textSelection = (TextSelection)dte.ActiveDocument.Selection;
|
|
|
+
|
|
|
+ if (fileArgumentParts.Length > 2)
|
|
|
+ {
|
|
|
+ if (int.TryParse(fileArgumentParts[2], out int column))
|
|
|
+ {
|
|
|
+ textSelection.MoveToLineAndOffset(line, column);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Console.Error.WriteLine("The column part of the argument must be a valid integer");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ textSelection.GotoLine(line, Select: true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Console.Error.WriteLine("The line part of the argument must be a valid integer");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ var mainWindow = dte.MainWindow;
|
|
|
+ mainWindow.Activate();
|
|
|
+ SetForegroundWindow(new IntPtr(mainWindow.HWnd));
|
|
|
+
|
|
|
+ MessageFilter.Revoke();
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static DTE FindInstanceEditingSolution(string solutionPath)
|
|
|
+ {
|
|
|
+ if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
|
|
|
+ ppenumMoniker.Reset();
|
|
|
+
|
|
|
+ var moniker = new IMoniker[1];
|
|
|
+
|
|
|
+ while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
|
|
|
+ {
|
|
|
+ string ppszDisplayName;
|
|
|
+
|
|
|
+ CreateBindCtx(0, out IBindCtx ppbc);
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ Marshal.ReleaseComObject(ppbc);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ppszDisplayName == null)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // The digits after the colon are the process ID
|
|
|
+ if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.16.0:[0-9]"))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
|
|
|
+ {
|
|
|
+ if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
|
|
|
+ {
|
|
|
+ if (NormalizePath(dte.Solution.FullName) == solutionPath)
|
|
|
+ return dte;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ Marshal.ReleaseComObject(pprot);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ static string NormalizePath(string path)
|
|
|
+ {
|
|
|
+ return new Uri(Path.GetFullPath(path)).LocalPath
|
|
|
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
|
|
+ .ToUpperInvariant();
|
|
|
+ }
|
|
|
+
|
|
|
+ #region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
|
|
|
+
|
|
|
+ private class MessageFilter : IOleMessageFilter
|
|
|
+ {
|
|
|
+ // Class containing the IOleMessageFilter
|
|
|
+ // thread error-handling functions
|
|
|
+
|
|
|
+ private static IOleMessageFilter _oldFilter;
|
|
|
+
|
|
|
+ // Start the filter
|
|
|
+ public static void Register()
|
|
|
+ {
|
|
|
+ IOleMessageFilter newFilter = new MessageFilter();
|
|
|
+ int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
|
|
|
+ if (ret != 0)
|
|
|
+ Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Done with the filter, close it
|
|
|
+ public static void Revoke()
|
|
|
+ {
|
|
|
+ int ret = CoRegisterMessageFilter(_oldFilter, out _);
|
|
|
+ if (ret != 0)
|
|
|
+ Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+ // IOleMessageFilter functions
|
|
|
+ // Handle incoming thread requests
|
|
|
+ int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
|
|
|
+ {
|
|
|
+ // Return the flag SERVERCALL_ISHANDLED
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Thread call was rejected, so try again.
|
|
|
+ int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
|
|
|
+ {
|
|
|
+ if (dwRejectType == 2)
|
|
|
+ // flag = SERVERCALL_RETRYLATER
|
|
|
+ {
|
|
|
+ // Retry the thread call immediately if return >= 0 & < 100
|
|
|
+ return 99;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Too busy; cancel call
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
|
|
|
+ {
|
|
|
+ // Return the flag PENDINGMSG_WAITDEFPROCESS
|
|
|
+ return 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Implement the IOleMessageFilter interface
|
|
|
+ [DllImport("ole32.dll")]
|
|
|
+ private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
|
|
|
+ }
|
|
|
+
|
|
|
+ [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
|
+ private interface IOleMessageFilter
|
|
|
+ {
|
|
|
+ [PreserveSig]
|
|
|
+ int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
|
|
|
+
|
|
|
+ [PreserveSig]
|
|
|
+ int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
|
|
|
+
|
|
|
+ [PreserveSig]
|
|
|
+ int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+ }
|
|
|
+}
|