Переглянути джерело

[3.2] C#: Add VisualStudio support

Ignacio Etcheverry 5 роки тому
батько
коміт
d8af79140e

+ 3 - 1
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.
 
 

+ 12 - 0
modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid>
+        <OutputType>Exe</OutputType>
+        <TargetFramework>net472</TargetFramework>
+        <LangVersion>7.2</LangVersion>
+    </PropertyGroup>
+    <ItemGroup>
+        <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
+        <PackageReference Include="EnvDTE" Version="8.0.2" />
+    </ItemGroup>
+</Project>

+ 270 - 0
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
+    }
+}

+ 6 - 0
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<string> 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)"));

+ 15 - 0
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)
         {

+ 6 - 0
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

+ 31 - 2
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<string>
+                    {
+                        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}";
             }

+ 3 - 1
modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj

@@ -20,7 +20,7 @@
     <AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="GodotTools.IdeMessaging" Version="1.1.0" />
+    <PackageReference Include="GodotTools.IdeMessaging" Version="1.1.1" />
     <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
@@ -37,5 +37,7 @@
     <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
     <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
     <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
+    <!-- Include it if this is an SCons build targeting Windows, or if it's not an SCons build but we're on Windows -->
+    <ProjectReference Include="..\GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) " />
   </ItemGroup>
 </Project>

+ 11 - 0
modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs

@@ -307,6 +307,11 @@ namespace GodotTools.Ides
                         var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
                         return await HandleDebugPlay(request);
                     },
+                    [StopPlayRequest.Id] = async (peer, content) =>
+                    {
+                        var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body);
+                        return await HandleStopPlay(request);
+                    },
                     [ReloadScriptsRequest.Id] = async (peer, content) =>
                     {
                         _ = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
@@ -343,6 +348,12 @@ namespace GodotTools.Ides
                 return Task.FromResult<Response>(new DebugPlayResponse());
             }
 
+            private static Task<Response> HandleStopPlay(StopPlayRequest request)
+            {
+                DispatchToMainThread(Internal.EditorRunStop);
+                return Task.FromResult<Response>(new StopPlayResponse());
+            }
+
             private static Task<Response> HandleReloadScripts()
             {
                 DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);

+ 0 - 1
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<WCHAR> buffer;
 		buffer.resize(dwBufferSize);
 		res = RegQueryValueExW(hKey, p_value_name.c_str(), 0, NULL, (LPBYTE)buffer.ptr(), &dwBufferSize);
 	}