Merge pull request #39784 from neikeq/3.2-visualstudio-support

[3.2] C#: Add Visual Studio support
This commit is contained in:
Rémi Verschelde 2020-06-26 20:49:35 +02:00 committed by GitHub
commit d6ff55f30b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 415 additions and 8 deletions

View File

@ -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.

View File

@ -25,6 +25,7 @@ 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);

View File

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

View File

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

View File

@ -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)"));

View File

@ -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)
{

View File

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

View File

@ -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}";
}

View File

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

View File

@ -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<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);
@ -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<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);
@ -351,8 +363,13 @@ namespace GodotTools.Ides
private static async Task<Response> 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}