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
}
}