From d8af79140ea019f2689931ffec549005c2f63285 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Tue, 23 Jun 2020 21:01:54 +0200 Subject: [PATCH] [3.2] C#: Add VisualStudio support --- .../mono/build_scripts/godot_tools_build.py | 4 +- .../GodotTools.OpenVisualStudio.csproj | 12 + .../GodotTools.OpenVisualStudio/Program.cs | 270 ++++++++++++++++++ .../ProjectGenerator.cs | 6 + .../GodotTools.ProjectEditor/ProjectUtils.cs | 15 + modules/mono/editor/GodotTools/GodotTools.sln | 6 + .../GodotTools/GodotTools/GodotSharpEditor.cs | 33 ++- .../GodotTools/GodotTools/GodotTools.csproj | 4 +- .../GodotTools/Ides/MessagingServer.cs | 11 + modules/mono/utils/mono_reg_utils.cpp | 1 - 10 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj create mode 100644 modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs diff --git a/modules/mono/build_scripts/godot_tools_build.py b/modules/mono/build_scripts/godot_tools_build.py index 7391e8790de..3bbbf29d3bd 100644 --- a/modules/mono/build_scripts/godot_tools_build.py +++ b/modules/mono/build_scripts/godot_tools_build.py @@ -15,7 +15,9 @@ def build_godot_tools(source, target, env): from .solution_builder import build_solution - build_solution(env, solution_path, build_config) + extra_msbuild_args = ["/p:GodotPlatform=" + env["platform"]] + + build_solution(env, solution_path, build_config, extra_msbuild_args) # No need to copy targets. The GodotTools csproj takes care of copying them. diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj new file mode 100644 index 00000000000..5b3ed0b1b7a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -0,0 +1,12 @@ + + + {EAFFF236-FA96-4A4D-BD23-0E51EF988277} + Exe + net472 + 7.2 + + + + + + diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs new file mode 100644 index 00000000000..affb2a47e79 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -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 + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index fb2beb6995a..679d5bb444f 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -12,6 +12,11 @@ namespace GodotTools.ProjectEditor private const string CoreApiProjectName = "GodotSharp"; private const string EditorApiProjectName = "GodotSharpEditor"; + public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; + public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}"; + + public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}"; + public static string GenGameProject(string dir, string name, IEnumerable compileItems) { string path = Path.Combine(dir, name + ".csproj"); @@ -19,6 +24,7 @@ namespace GodotTools.ProjectEditor ProjectPropertyGroupElement mainGroup; var root = CreateLibraryProject(name, "Debug", out mainGroup); + mainGroup.SetProperty("ProjectTypeGuids", GodotDefaultProjectTypeGuids); mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)")); mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj")); mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)")); diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 9474fca8d75..ef465190645 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -168,6 +168,21 @@ namespace GodotTools.ProjectEditor return result.ToArray(); } + public static void EnsureHasProjectTypeGuids(MSBuildProject project) + { + var root = project.Root; + + bool found = root.PropertyGroups.Any(pg => + string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids")); + + if (found) + return; + + root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids); + + project.HasUnsavedChanges = true; + } + /// Simple function to make sure the Api assembly references are configured correctly public static void FixApiHintPath(MSBuildProject project) { diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 6f7d44bec2e..19c00241dad 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU + {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 0fc3280ae50..d9af134e2b3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using GodotTools.Ides; using GodotTools.Ides.Rider; using GodotTools.Internals; @@ -250,7 +251,31 @@ namespace GodotTools // Not an error. Tells the caller to fallback to the global external editor settings or the built-in editor. return Error.Unavailable; case ExternalEditorId.VisualStudio: - throw new NotSupportedException(); + { + string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); + + var args = new List + { + GodotSharpDirs.ProjectSlnPath, + line >= 0 ? $"{scriptPath};{line + 1};{col + 1}" : scriptPath + }; + + string command = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.exe"); + + try + { + if (Godot.OS.IsStdoutVerbose()) + Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}"); + + OS.RunProcess(command, args); + } + catch (Exception e) + { + GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'"); + } + + break; + } case ExternalEditorId.VisualStudioForMac: goto case ExternalEditorId.MonoDevelop; case ExternalEditorId.Rider: @@ -468,6 +493,9 @@ namespace GodotTools // Apply the other fixes only after configurations have been migrated + // Make sure the existing project has the ProjectTypeGuids property (for VisualStudio) + ProjectUtils.EnsureHasProjectTypeGuids(msbuildProject); + // Make sure the existing project has Api assembly references configured correctly ProjectUtils.FixApiHintPath(msbuildProject); @@ -511,7 +539,8 @@ namespace GodotTools if (OS.IsWindows) { - settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + + settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudio}" + + $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" + $",Visual Studio Code:{(int)ExternalEditorId.VsCode}" + $",JetBrains Rider:{(int)ExternalEditorId.Rider}"; } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index efa15f9ed8b..df0eb26be15 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -20,7 +20,7 @@ False - + @@ -37,5 +37,7 @@ + + diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 04b156e6dba..f7130caff6b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -307,6 +307,11 @@ namespace GodotTools.Ides var request = JsonConvert.DeserializeObject(content.Body); return await HandleDebugPlay(request); }, + [StopPlayRequest.Id] = async (peer, content) => + { + var request = JsonConvert.DeserializeObject(content.Body); + return await HandleStopPlay(request); + }, [ReloadScriptsRequest.Id] = async (peer, content) => { _ = JsonConvert.DeserializeObject(content.Body); @@ -343,6 +348,12 @@ namespace GodotTools.Ides return Task.FromResult(new DebugPlayResponse()); } + private static Task HandleStopPlay(StopPlayRequest request) + { + DispatchToMainThread(Internal.EditorRunStop); + return Task.FromResult(new StopPlayResponse()); + } + private static Task HandleReloadScripts() { DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts); diff --git a/modules/mono/utils/mono_reg_utils.cpp b/modules/mono/utils/mono_reg_utils.cpp index c1cd5f1db43..67857810a39 100644 --- a/modules/mono/utils/mono_reg_utils.cpp +++ b/modules/mono/utils/mono_reg_utils.cpp @@ -77,7 +77,6 @@ LONG _RegKeyQueryString(HKEY hKey, const String &p_value_name, String &r_value) if (res == ERROR_MORE_DATA) { // dwBufferSize now contains the actual size - Vector buffer; buffer.resize(dwBufferSize); res = RegQueryValueExW(hKey, p_value_name.c_str(), 0, NULL, (LPBYTE)buffer.ptr(), &dwBufferSize); }