godot/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs

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
                dte = TryVisualStudioLaunch("VisualStudio.DTE.17.0");

                if (dte == null)
                {
                    // Launch of VS 2022 failed, fallback to 2019
                    dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0");

                    if (dte == null)
                    {
                        Console.Error.WriteLine("Visual Studio not found");
                        return 1;
                    }
                }

                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(mainWindow.HWnd);

                MessageFilter.Revoke();
            }

            return 0;
        }

        private static DTE? TryVisualStudioLaunch(string version)
        {
            try
            {
                var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
                var dte = (DTE?)Activator.CreateInstance(visualStudioDteType!);

                return dte;
            }
            catch (COMException)
            {
                return null;
            }
        }

        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.1[6-7].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;
        }

        private 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)
            {
                // flag = SERVERCALL_RETRYLATER
                if (dwRejectType == 2)
                {
                    // 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
    }
}