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.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 326c49f0965..890138911a0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -25,8 +25,9 @@ namespace GodotTools.Core bool rooted = path.IsAbsolutePath(); path = path.Replace('\\', '/'); + path = path[path.Length - 1] == '/' ? path.Substring(0, path.Length - 1) : path; - string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim(); 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..17f33395605 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -12,6 +12,7 @@ using GodotTools.IdeMessaging; using GodotTools.IdeMessaging.Requests; using GodotTools.IdeMessaging.Utils; using GodotTools.Internals; +using GodotTools.Utils; using Newtonsoft.Json; using Directory = System.IO.Directory; using File = System.IO.File; @@ -307,6 +308,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); @@ -336,13 +342,19 @@ namespace GodotTools.Ides DispatchToMainThread(() => { GodotSharpEditor.Instance.CurrentPlaySettings = - new PlaySettings(request.DebuggerHost, request.DebuggerPort, buildBeforePlaying: true); + new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); Internal.EditorRunPlay(); GodotSharpEditor.Instance.CurrentPlaySettings = null; }); 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); @@ -351,8 +363,13 @@ namespace GodotTools.Ides private static async Task HandleCodeCompletionRequest(CodeCompletionRequest request) { + // This is needed if the "resource path" part of the path is case insensitive. + // However, it doesn't fix resource loading if the rest of the path is also case insensitive. + string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); + var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile}; - response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, response.ScriptFile)); + response.Suggestions = await Task.Run(() => + Internal.CodeCompletionRequest(response.Kind, scriptFileLocalized ?? request.ScriptFile)); return response; } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs new file mode 100644 index 00000000000..b6716477f79 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs @@ -0,0 +1,48 @@ +using System; +using Godot; +using GodotTools.Core; +using JetBrains.Annotations; +using Path = System.IO.Path; + +namespace GodotTools.Utils +{ + public static class FsPathUtils + { + private static readonly string ResourcePath = ProjectSettings.GlobalizePath("res://"); + + private static bool PathStartsWithAlreadyNorm(this string childPath, string parentPath) + { + // This won't work for Linux/macOS case insensitive file systems, but it's enough for our current problems + bool caseSensitive = !OS.IsWindows; + + string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar; + string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar; + + return childPathNorm.StartsWith(parentPathNorm, + caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + public static bool PathStartsWith(this string childPath, string parentPath) + { + string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar; + string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar; + + return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm); + } + + [CanBeNull] + public static string LocalizePathWithCaseChecked(string path) + { + string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar; + string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar; + + if (!pathNorm.PathStartsWithAlreadyNorm(resourcePathNorm)) + return null; + + string result = "res://" + pathNorm.Substring(resourcePathNorm.Length); + + // Remove the last separator we added + return result.Substring(0, result.Length - 1); + } + } +} 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); }