diff --git a/CHANGELOG.md b/CHANGELOG.md index ec430d1..fd79e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Code Editor Package for Visual Studio +## [2.0.17] - 2022-12-06 + +Integration: + +- Fix rare deadlocks while discovering or launching Visual Studio on Windows. +- Improve launching Visual Studio on macOs. + +Project generation: + +- Include analyzers from response files. +- Update supported C# versions. +- Performance improvements. + + ## [2.0.16] - 2022-06-08 Integration: @@ -7,7 +21,6 @@ Integration: - Prevent ADB Refresh while being in safe-mode with a URP project - Fixed an issue keeping the progress bar visible even after opening a script with Visual Studio. - ## [2.0.15] - 2022-03-21 Integration: diff --git a/Editor/AppleEventIntegration~/AppleEventIntegration/main.mm b/Editor/AppleEventIntegration~/AppleEventIntegration/main.mm index 678b765..2a1ee89 100644 --- a/Editor/AppleEventIntegration~/AppleEventIntegration/main.mm +++ b/Editor/AppleEventIntegration~/AppleEventIntegration/main.mm @@ -3,7 +3,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - + #import #import @@ -11,7 +11,7 @@ #define keyFileSender 1179872868 // 16 bit aligned legacy struct - this should total 20 bytes -struct SelectionRange +typedef struct _SelectionRange { int16_t unused1; // 0 (not used) int16_t lineNum; // line to select (<0 to specify range) @@ -19,7 +19,7 @@ struct SelectionRange int32_t endRange; // end of selection range (if line < 0) int32_t unused2; // 0 (not used) int32_t theDate; // modification date/time -} __attribute__((packed)); +} __attribute__((packed)) SelectionRange; static NSString* MakeNSString(const char* str) { @@ -200,18 +200,13 @@ static NSRunningApplication* QueryRunningApplicationOpenedOnSolution(NSString* a static NSRunningApplication* LaunchApplicationOnSolution(NSString* appPath, NSString* solutionPath) { - NSURL* appUrl = [NSURL fileURLWithPath: appPath]; - NSMutableDictionary* config = [[NSMutableDictionary alloc] init]; - - NSRunningApplication* runningApp = [[NSWorkspace sharedWorkspace] - launchApplicationAtURL: appUrl + return [[NSWorkspace sharedWorkspace] + launchApplicationAtURL: [NSURL fileURLWithPath: appPath] options: NSWorkspaceLaunchDefault | NSWorkspaceLaunchNewInstance - configuration: config + configuration: @{ + NSWorkspaceLaunchConfigurationArguments: @[ solutionPath ], + } error: nil]; - - OpenFileAtLineWithAppleEvent(runningApp, solutionPath, -1); - - return runningApp; } static NSRunningApplication* QueryOrLaunchApplication(NSString* appPath, NSString* solutionPath) diff --git a/Editor/AsyncOperation.cs b/Editor/AsyncOperation.cs index 0644347..c4f489b 100644 --- a/Editor/AsyncOperation.cs +++ b/Editor/AsyncOperation.cs @@ -11,20 +11,24 @@ namespace Microsoft.Unity.VisualStudio.Editor internal class AsyncOperation { private readonly Func _producer; + private readonly Func _exceptionHandler; + private readonly Action _finalHandler; private readonly ManualResetEventSlim _resetEvent; private T _result; private Exception _exception; - private AsyncOperation(Func producer) + private AsyncOperation(Func producer, Func exceptionHandler, Action finalHandler) { _producer = producer; + _exceptionHandler = exceptionHandler; + _finalHandler = finalHandler; _resetEvent = new ManualResetEventSlim(initialState: false); } - public static AsyncOperation Run(Func producer) + public static AsyncOperation Run(Func producer, Func exceptionHandler = null, Action finalHandler = null) { - var task = new AsyncOperation(producer); + var task = new AsyncOperation(producer, exceptionHandler, finalHandler); task.Run(); return task; } @@ -40,9 +44,15 @@ namespace Microsoft.Unity.VisualStudio.Editor catch (Exception e) { _exception = e; + + if (_exceptionHandler != null) + { + _result = _exceptionHandler(e); + } } finally { + _finalHandler?.Invoke(); _resetEvent.Set(); } }); diff --git a/Editor/COMIntegration/COMIntegration~/COMIntegration.cpp b/Editor/COMIntegration/COMIntegration~/COMIntegration.cpp index 375f9fd..f849ff8 100644 --- a/Editor/COMIntegration/COMIntegration~/COMIntegration.cpp +++ b/Editor/COMIntegration/COMIntegration~/COMIntegration.cpp @@ -205,6 +205,32 @@ static bool StartVisualStudioProcess( return true; } +static bool +MonikerIsVisualStudioProcess(const win::ComPtr &moniker, const win::ComPtr &bindCtx, const DWORD dwProcessId = 0) { + LPOLESTR oleMonikerName; + if (FAILED(moniker->GetDisplayName(bindCtx, nullptr, &oleMonikerName))) + return false; + + std::wstring monikerName(oleMonikerName); + + // VisualStudio Moniker is "!VisualStudio.DTE.$Version:$PID" + // Example "!VisualStudio.DTE.14.0:1234" + + if (monikerName.find(L"!VisualStudio.DTE") != 0) + return false; + + if (dwProcessId == 0) + return true; + + std::wstringstream suffixStream; + suffixStream << ":"; + suffixStream << dwProcessId; + + std::wstring suffix(suffixStream.str()); + + return monikerName.length() - suffix.length() == monikerName.find(suffix); +} + static win::ComPtr FindRunningVisualStudioWithSolution( const std::filesystem::path &visualStudioExecutablePath, const std::filesystem::path &solutionPath) @@ -232,6 +258,9 @@ static win::ComPtr FindRunningVisualStudioWithSolution( win::ComPtr moniker; ULONG monikersFetched = 0; while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) { + if (!MonikerIsVisualStudioProcess(moniker, bindCtx)) + continue; + if (FAILED(ROT->GetObject(moniker, &punk))) continue; @@ -285,29 +314,6 @@ static win::ComPtr FindRunningVisualStudioWithSolution( return nullptr; } -static bool -MonikerIsVisualStudioProcess(const win::ComPtr &moniker, const win::ComPtr &bindCtx, const DWORD dwProcessId) { - LPOLESTR oleMonikerName; - if (FAILED(moniker->GetDisplayName(bindCtx, nullptr, &oleMonikerName))) - return false; - - std::wstring monikerName(oleMonikerName); - - // VisualStudio Moniker is "!VisualStudio.DTE.$Version:$PID" - // Example "!VisualStudio.DTE.14.0:1234" - - if (monikerName.find(L"!VisualStudio.DTE") != 0) - return false; - - std::wstringstream suffixStream; - suffixStream << ":"; - suffixStream << dwProcessId; - - std::wstring suffix(suffixStream.str()); - - return monikerName.length() - suffix.length() == monikerName.find(suffix); -} - static win::ComPtr FindRunningVisualStudioWithPID(const DWORD dwProcessId) { win::ComPtr punk = nullptr; win::ComPtr dte = nullptr; @@ -329,10 +335,10 @@ static win::ComPtr FindRunningVisualStudioWithPID(const DWORD dwPr win::ComPtr moniker; ULONG monikersFetched = 0; while (SUCCEEDED(enumMoniker->Next(1, &moniker, &monikersFetched)) && monikersFetched) { - if (FAILED(ROT->GetObject(moniker, &punk))) + if (!MonikerIsVisualStudioProcess(moniker, bindCtx, dwProcessId)) continue; - if (!MonikerIsVisualStudioProcess(moniker, bindCtx, dwProcessId)) + if (FAILED(ROT->GetObject(moniker, &punk))) continue; punk.As(&dte); diff --git a/Editor/COMIntegration/COMIntegration~/howtobuild.txt b/Editor/COMIntegration/COMIntegration~/howtobuild.txt index 3045f70..60c7eab 100644 --- a/Editor/COMIntegration/COMIntegration~/howtobuild.txt +++ b/Editor/COMIntegration/COMIntegration~/howtobuild.txt @@ -1,6 +1,9 @@ Direct style: cl /EHsc /std:c++17 COMIntegration.cpp /link Shlwapi.lib /out:"..\Release\COMIntegration.exe" +For a debug build with PDB: +cl /EHsc /std:c++17 /Z7 /DEBUG:FULL COMIntegration.cpp /link Shlwapi.lib /out:"..\Release\COMIntegration.exe" + CMake style: cmake ../COMIntegration~ -B ./build cmake --build ./build --config=release -- /p:OutDir=.. diff --git a/Editor/COMIntegration/Release/COMIntegration.exe b/Editor/COMIntegration/Release/COMIntegration.exe index cdaaa93..70080ff 100644 Binary files a/Editor/COMIntegration/Release/COMIntegration.exe and b/Editor/COMIntegration/Release/COMIntegration.exe differ diff --git a/Editor/Discovery.cs b/Editor/Discovery.cs index 8b6f93e..6299022 100644 --- a/Editor/Discovery.cs +++ b/Editor/Discovery.cs @@ -150,31 +150,15 @@ namespace Microsoft.Unity.VisualStudio.Editor if (string.IsNullOrWhiteSpace(progpath)) return Enumerable.Empty(); - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = progpath, - Arguments = "-prerelease -format json -utf8", - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - } - }; + var result = ProcessRunner.StartAndWaitForExit(progpath, "-prerelease -format json -utf8"); - using (process) - { - var json = string.Empty; + if (!result.Success) + throw new Exception($"Failure while running vswhere: {result.Error}"); - process.OutputDataReceived += (o, e) => json += e.Data; - process.Start(); - process.BeginOutputReadLine(); - process.WaitForExit(); - - var result = VsWhereResult.FromJson(json); - return result.ToVisualStudioInstallations(); - } + // Do not catch any JsonException here, this will be handled by the caller + return VsWhereResult + .FromJson(result.Output) + .ToVisualStudioInstallations(); } } } diff --git a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration index e6fa05a..d37b4ad 100644 Binary files a/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration and b/Editor/Plugins/AppleEventIntegration.bundle/Contents/MacOS/AppleEventIntegration differ diff --git a/Editor/ProcessRunner.cs b/Editor/ProcessRunner.cs new file mode 100644 index 0000000..ebb8ecb --- /dev/null +++ b/Editor/ProcessRunner.cs @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class ProcessRunnerResult + { + public bool Success { get; set; } + public string Output { get; set; } + public string Error { get; set; } + } + + internal static class ProcessRunner + { + public const int DefaultTimeoutInMilliseconds = 300000; + + public static ProcessStartInfo ProcessStartInfoFor(string filename, string arguments) + { + return new ProcessStartInfo + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = filename, + Arguments = arguments + }; + } + + public static ProcessRunnerResult StartAndWaitForExit(string filename, string arguments, int timeoutms = DefaultTimeoutInMilliseconds, Action onOutputReceived = null) + { + return StartAndWaitForExit(ProcessStartInfoFor(filename, arguments), timeoutms, onOutputReceived); + } + + public static ProcessRunnerResult StartAndWaitForExit(ProcessStartInfo processStartInfo, int timeoutms = DefaultTimeoutInMilliseconds, Action onOutputReceived = null) + { + var process = new Process { StartInfo = processStartInfo }; + + using (process) + { + var sbOutput = new StringBuilder(); + var sbError = new StringBuilder(); + + var outputSource = new TaskCompletionSource(); + var errorSource = new TaskCompletionSource(); + + process.OutputDataReceived += (_, e) => + { + Append(sbOutput, e.Data, outputSource); + if (onOutputReceived != null && e.Data != null) + onOutputReceived(e.Data); + }; + process.ErrorDataReceived += (_, e) => Append(sbError, e.Data, errorSource); + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + var run = Task.Run(() => process.WaitForExit(timeoutms)); + var processTask = Task.WhenAll(run, outputSource.Task, errorSource.Task); + + if (Task.WhenAny(Task.Delay(timeoutms), processTask).Result == processTask && run.Result) + return new ProcessRunnerResult {Success = true, Error = sbError.ToString(), Output = sbOutput.ToString()}; + + try + { + process.Kill(); + } + catch + { + /* ignore */ + } + + return new ProcessRunnerResult {Success = false, Error = sbError.ToString(), Output = sbOutput.ToString()}; + } + } + + private static void Append(StringBuilder sb, string data, TaskCompletionSource taskSource) + { + if (data == null) + { + taskSource.SetResult(true); + return; + } + + sb?.Append(data); + } + } +} diff --git a/Editor/ProcessRunner.cs.meta b/Editor/ProcessRunner.cs.meta new file mode 100644 index 0000000..d0418ef --- /dev/null +++ b/Editor/ProcessRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 957a5f2d2660a894d926660de2a9d577 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ProjectGeneration/ProjectGeneration.cs b/Editor/ProjectGeneration/ProjectGeneration.cs index df9b2da..b94a90e 100644 --- a/Editor/ProjectGeneration/ProjectGeneration.cs +++ b/Editor/ProjectGeneration/ProjectGeneration.cs @@ -43,6 +43,7 @@ namespace Microsoft.Unity.VisualStudio.Editor public IAssemblyNameProvider AssemblyNameProvider => m_AssemblyNameProvider; public string ProjectDirectory { get; } + // Use this to have the same newline ending on all platforms for consistency. const string k_WindowsNewline = "\r\n"; const string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject"; @@ -126,11 +127,11 @@ namespace Microsoft.Unity.VisualStudio.Editor var affectedNames = affected .Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)) .Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => - name.Split(new[] {".dll"}, StringSplitOptions.RemoveEmptyEntries)[0]); + name.Split(new[] { ".dll" }, StringSplitOptions.RemoveEmptyEntries)[0]); var reimportedNames = reimported .Select(asset => m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset)) .Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => - name.Split(new[] {".dll"}, StringSplitOptions.RemoveEmptyEntries)[0]); + name.Split(new[] { ".dll" }, StringSplitOptions.RemoveEmptyEntries)[0]); var affectedAndReimported = new HashSet(affectedNames.Concat(reimportedNames)); foreach (var assembly in allProjectAssemblies) @@ -381,19 +382,19 @@ namespace Microsoft.Unity.VisualStudio.Editor { var filename = EscapedRelativePathFor(asset, out var packageInfo); - builder.Append($" <{tag} Include=\"").Append(filename); + builder.Append(" <").Append(tag).Append(@" Include=""").Append(filename); if (Path.IsPathRooted(filename) && packageInfo != null) { // We are outside the Unity project and using a package context var linkPath = SkipPathPrefix(asset.NormalizePathSeparators(), packageInfo.assetPath.NormalizePathSeparators()); - builder.Append("\">").Append(k_WindowsNewline); + builder.Append(@""">").Append(k_WindowsNewline); builder.Append(" ").Append(linkPath).Append("").Append(k_WindowsNewline); builder.Append($" ").Append(k_WindowsNewline); } else { - builder.Append("\" />").Append(k_WindowsNewline); + builder.Append(@""" />").Append(k_WindowsNewline); } } @@ -504,7 +505,8 @@ namespace Microsoft.Unity.VisualStudio.Editor Dictionary allAssetsProjectParts, ResponseFileData[] responseFilesData) { - var projectBuilder = new StringBuilder(ProjectHeader(assembly, responseFilesData)); + ProjectHeader(assembly, responseFilesData, out StringBuilder projectBuilder); + var references = new List(); projectBuilder.Append(@" ").Append(k_WindowsNewline); @@ -563,7 +565,7 @@ namespace Microsoft.Unity.VisualStudio.Editor // If the current assembly is a Player project, we want to project-reference the corresponding Player project var referenceName = m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, reference.name); - projectBuilder.Append(" ").Append(k_WindowsNewline); + projectBuilder.Append(@" ").Append(k_WindowsNewline); projectBuilder.Append(" {").Append(ProjectGuid(referenceName)).Append("}").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(referenceName).Append("").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(k_WindowsNewline); @@ -595,7 +597,7 @@ namespace Microsoft.Unity.VisualStudio.Editor private void AppendReference(string fullReference, StringBuilder projectBuilder) { var escapedFullPath = EscapedRelativePathFor(fullReference, out _); - projectBuilder.Append(" ").Append(k_WindowsNewline); + projectBuilder.Append(@" ").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(escapedFullPath).Append("").Append(k_WindowsNewline); projectBuilder.Append(" ").Append(k_WindowsNewline); } @@ -632,25 +634,77 @@ namespace Microsoft.Unity.VisualStudio.Editor return targetLanguageVersion; } - private string ProjectHeader( + private static IEnumerable GetOtherArguments(ResponseFileData[] responseFilesData, HashSet names) + { + var lines = responseFilesData + .SelectMany(x => x.OtherArguments) + .Where(l => !string.IsNullOrEmpty(l)) + .Select(l => l.Trim()) + .Where(l => l.StartsWith("/") || l.StartsWith("-")); + + foreach (var argument in lines) + { + var index = argument.IndexOf(":", StringComparison.Ordinal); + if (index == -1) + continue; + + var key = argument + .Substring(1, index - 1) + .Trim(); + + if (!names.Contains(key)) + continue; + + if (argument.Length <= index) + continue; + + yield return argument + .Substring(index + 1) + .Trim(); + } + } + + private string[] GetAnalyzers(Assembly assembly, ResponseFileData[] responseFilesData, out string rulesetPath) + { + rulesetPath = null; + + if (m_CurrentInstallation == null || !m_CurrentInstallation.SupportsAnalyzers) + return Array.Empty(); + + // Analyzers provided by VisualStudio + List analyzers = new List(m_CurrentInstallation.GetAnalyzers()); + +#if UNITY_2020_2_OR_NEWER + // Analyzers + ruleset provided by Unity + analyzers.AddRange(assembly.compilerOptions.RoslynAnalyzerDllPaths); + + rulesetPath = assembly + .compilerOptions + .RoslynAnalyzerRulesetPath + .MakeAbsolutePath() + .NormalizePathSeparators(); +#endif + + // Analyzers provided by csc.rsp + analyzers.AddRange(GetOtherArguments(responseFilesData, new HashSet(new[] { "analyzer", "a" }))); + + return analyzers + .Where(a => !string.IsNullOrEmpty(a)) + .Select(a => a.MakeAbsolutePath().NormalizePathSeparators()) + .Distinct() + .ToArray(); + } + + private void ProjectHeader( Assembly assembly, - ResponseFileData[] responseFilesData + ResponseFileData[] responseFilesData, + out StringBuilder headerBuilder ) { var projectType = ProjectTypeOf(assembly.name); - string rulesetPath = null; - var analyzers = Array.Empty(); + var analyzers = GetAnalyzers(assembly, responseFilesData, out var rulesetPath); - if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsAnalyzers) - { - analyzers = m_CurrentInstallation.GetAnalyzers(); -#if UNITY_2020_2_OR_NEWER - analyzers = analyzers != null ? analyzers.Concat(assembly.compilerOptions.RoslynAnalyzerDllPaths).ToArray() : assembly.compilerOptions.RoslynAnalyzerDllPaths; - rulesetPath = assembly.compilerOptions.RoslynAnalyzerRulesetPath; -#endif - } - - var projectProperties = new ProjectProperties() + var projectProperties = new ProjectProperties { ProjectGuid = ProjectGuid(assembly), LangVersion = GetLangVersion(assembly), @@ -658,9 +712,9 @@ namespace Microsoft.Unity.VisualStudio.Editor RootNamespace = GetRootNamespace(assembly), OutputPath = assembly.outputPath, // Analyzers - Analyzers = analyzers, RulesetPath = rulesetPath, // RSP alterable + Analyzers = analyzers, Defines = assembly.defines.Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray(), Unsafe = assembly.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe), // VSTU Flavoring @@ -670,7 +724,7 @@ namespace Microsoft.Unity.VisualStudio.Editor FlavoringPackageVersion = VisualStudioIntegration.PackageVersion(), }; - return GetProjectHeader(projectProperties); + GetProjectHeader(projectProperties, out headerBuilder); } private enum ProjectType @@ -696,102 +750,86 @@ namespace Microsoft.Unity.VisualStudio.Editor return ProjectType.Game; } - private string GetProjectHeader(ProjectProperties properties) + private void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder) { - var header = new[] - { - $@"", - $@"", - $@" ", - $@" {properties.LangVersion}", - $@" ", - $@" ", - $@" Debug", - $@" AnyCPU", - $@" 10.0.20506", - $@" 2.0", - $@" {properties.RootNamespace}", - $@" {{{properties.ProjectGuid}}}", - $@" Library", - $@" Properties", - $@" {properties.AssemblyName}", - $@" v4.7.1", - $@" 512", - $@" .", - $@" ", - $@" ", - $@" true", - $@" full", - $@" false", - $@" {properties.OutputPath}", - $@" {string.Join(";", properties.Defines)}", - $@" prompt", - $@" 4", - $@" 0169", - $@" {properties.Unsafe}", - $@" ", - $@" ", - $@" pdbonly", - $@" true", - $@" Temp\bin\Release\", - $@" prompt", - $@" 4", - $@" 0169", - $@" {properties.Unsafe}", - $@" " - }; + headerBuilder = new StringBuilder(); - var forceExplicitReferences = new[] - { - $@" ", - $@" true", - $@" true", - $@" false", - $@" false", - $@" false", - $@" " - }; + //Header + headerBuilder.Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.LangVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" Debug").Append(k_WindowsNewline); + headerBuilder.Append(@" AnyCPU").Append(k_WindowsNewline); + headerBuilder.Append(@" 10.0.20506").Append(k_WindowsNewline); + headerBuilder.Append(@" 2.0").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.RootNamespace).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" {").Append(properties.ProjectGuid).Append(@"}").Append(k_WindowsNewline); + headerBuilder.Append(@" Library").Append(k_WindowsNewline); + headerBuilder.Append(@" Properties").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.AssemblyName).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" v4.7.1").Append(k_WindowsNewline); + headerBuilder.Append(@" 512").Append(k_WindowsNewline); + headerBuilder.Append(@" .").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" full").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.OutputPath).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(string.Join(";", properties.Defines)).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" prompt").Append(k_WindowsNewline); + headerBuilder.Append(@" 4").Append(k_WindowsNewline); + headerBuilder.Append(@" 0169").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.Unsafe).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" pdbonly").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" Temp\bin\Release\").Append(k_WindowsNewline); + headerBuilder.Append(@" prompt").Append(k_WindowsNewline); + headerBuilder.Append(@" 4").Append(k_WindowsNewline); + headerBuilder.Append(@" 0169").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.Unsafe).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); - var flavoring = new[] - { - $@" ", - $@" {{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}", - $@" Package", - $@" {properties.FlavoringPackageVersion}", - $@" {properties.FlavoringProjectType}", - $@" {properties.FlavoringBuildTarget}", - $@" {properties.FlavoringUnityVersion}", - $@" " - }; + // Explicit references + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" true").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" false").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); - var footer = new[] - { - @"" - }; - - var lines = header - .Concat(forceExplicitReferences) - .Concat(flavoring) - .ToList(); + // Flavoring + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" {E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}").Append(k_WindowsNewline); + headerBuilder.Append(@" Package").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringPackageVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringProjectType).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringBuildTarget).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.FlavoringUnityVersion).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); if (!string.IsNullOrEmpty(properties.RulesetPath)) { - lines.Add(@" "); - lines.Add($" {properties.RulesetPath.MakeAbsolutePath().NormalizePathSeparators()}"); - lines.Add(@" "); + headerBuilder.Append(@" ").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(properties.RulesetPath).Append(@"").Append(k_WindowsNewline); + headerBuilder.Append(@" ").Append(k_WindowsNewline); } if (properties.Analyzers.Any()) { - lines.Add(@" "); - foreach (var analyzer in properties.Analyzers.Distinct()) + headerBuilder.Append(@" ").Append(k_WindowsNewline); + foreach (var analyzer in properties.Analyzers) { - lines.Add($@" "); + headerBuilder.Append(@" ").Append(k_WindowsNewline); } - lines.Add(@" "); + headerBuilder.Append(@" ").Append(k_WindowsNewline); } - - return string.Join(k_WindowsNewline, lines.Concat(footer)); } private static string GetProjectFooter() @@ -885,7 +923,7 @@ namespace Microsoft.Unity.VisualStudio.Editor if (array == null || array.Length == 0) { // HideSolution by default - array = new [] { + array = new[] { new SolutionProperties() { Name = "SolutionProperties", Type = "preSolution", diff --git a/Editor/SolutionParser.cs b/Editor/SolutionParser.cs index 35c9346..ebdd635 100644 --- a/Editor/SolutionParser.cs +++ b/Editor/SolutionParser.cs @@ -10,9 +10,9 @@ namespace Microsoft.Unity.VisualStudio.Editor internal static class SolutionParser { // Compared to the bridge implementation, we are not returning "{" "}" from Guids - private static readonly Regex ProjectDeclaration = new Regex(@"Project\(\""{(?.*?)}\""\)\s+=\s+\""(?.*?)\"",\s+\""(?.*?)\"",\s+\""{(?.*?)}\""(?.*?)\bEndProject\b", RegexOptions.Singleline | RegexOptions.ExplicitCapture); - private static readonly Regex PropertiesDeclaration = new Regex(@"GlobalSection\((?([\w]+Properties|NestedProjects))\)\s+=\s+(?(?:post|pre)Solution)(?.*?)EndGlobalSection", RegexOptions.Singleline | RegexOptions.ExplicitCapture); - private static readonly Regex PropertiesEntryDeclaration = new Regex(@"^\s*(?.*?)=(?.*?)$", RegexOptions.Multiline | RegexOptions.ExplicitCapture); + private static readonly Regex ProjectDeclaration = new Regex(@"Project\(\""{(?.*?)}\""\)\s+=\s+\""(?.*?)\"",\s+\""(?.*?)\"",\s+\""{(?.*?)}\""(?.*?)\bEndProject\b", RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.Compiled); + private static readonly Regex PropertiesDeclaration = new Regex(@"GlobalSection\((?([\w]+Properties|NestedProjects))\)\s+=\s+(?(?:post|pre)Solution)(?.*?)EndGlobalSection", RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.Compiled); + private static readonly Regex PropertiesEntryDeclaration = new Regex(@"^\s*(?.*?)=(?.*?)$", RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled); public static Solution ParseSolutionFile(string filename, IFileIO fileIO) { diff --git a/Editor/VSWhere/vswhere.exe b/Editor/VSWhere/vswhere.exe index 1731aa6..ab3c7f3 100644 Binary files a/Editor/VSWhere/vswhere.exe and b/Editor/VSWhere/vswhere.exe differ diff --git a/Editor/VisualStudioEditor.cs b/Editor/VisualStudioEditor.cs index 06816b5..7367d9e 100644 --- a/Editor/VisualStudioEditor.cs +++ b/Editor/VisualStudioEditor.cs @@ -4,7 +4,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -12,6 +11,8 @@ using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; using Unity.CodeEditor; +using System.Threading; +using System.Collections.Concurrent; [assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")] [assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")] @@ -287,6 +288,14 @@ namespace Microsoft.Unity.VisualStudio.Editor return false; } + private enum COMIntegrationState + { + Running, + DisplayProgressBar, + ClearProgressBar, + Exited + } + private bool OpenWindowsApp(string path, int line) { var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe"); @@ -309,44 +318,67 @@ namespace Microsoft.Unity.VisualStudio.Editor solution = solution.Replace("^", "^^"); } - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = progpath, - Arguments = $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}", - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - StandardOutputEncoding = System.Text.Encoding.Unicode, - RedirectStandardError = true, - StandardErrorEncoding = System.Text.Encoding.Unicode, - } - }; - var result = process.Start(); + + var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}"); + psi.StandardOutputEncoding = System.Text.Encoding.Unicode; + psi.StandardErrorEncoding = System.Text.Encoding.Unicode; - while (!process.StandardOutput.EndOfStream) - { - var outputLine = process.StandardOutput.ReadLine(); - if (outputLine == "displayProgressBar") - { - EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f); - } + // inter thread communication + var messages = new BlockingCollection(); - if (outputLine == "clearprogressbar") + var asyncStart = AsyncOperation.Run( + () => ProcessRunner.StartAndWaitForExit(psi, onOutputReceived: data => OnOutputReceived(data, messages)), + e => new ProcessRunnerResult {Success = false, Error = e.Message, Output = string.Empty}, + () => messages.Add(COMIntegrationState.Exited) + ); + + MonitorCOMIntegration(messages); + + var result = asyncStart.Result; + + if (!result.Success && !string.IsNullOrWhiteSpace(result.Error)) + Debug.LogError($"Error while starting Visual Studio: {result.Error}"); + + return result.Success; + } + + private static void MonitorCOMIntegration(BlockingCollection messages) + { + var displayingProgress = false; + COMIntegrationState state; + + do + { + state = messages.Take(); + switch (state) { - EditorUtility.ClearProgressBar(); + case COMIntegrationState.ClearProgressBar: + EditorUtility.ClearProgressBar(); + displayingProgress = false; + break; + case COMIntegrationState.DisplayProgressBar: + EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f); + displayingProgress = true; + break; } + } while (state != COMIntegrationState.Exited); + + // Make sure the progress bar is properly cleared in case of COMIntegration failure + if (displayingProgress) + EditorUtility.ClearProgressBar(); + } + + private static readonly COMIntegrationState[] ProgressBarCommands = {COMIntegrationState.DisplayProgressBar, COMIntegrationState.ClearProgressBar}; + private static void OnOutputReceived(string data, BlockingCollection messages) + { + if (data == null) + return; + + foreach (var cmd in ProgressBarCommands) + { + if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0) + messages.Add(cmd); } - - var errorOutput = process.StandardError.ReadToEnd(); - if (!string.IsNullOrEmpty(errorOutput)) - { - Console.WriteLine("Error: \n" + errorOutput); - } - - process.WaitForExit(); - return result; } [DllImport("AppleEventIntegration")] diff --git a/Editor/VisualStudioInstallation.cs b/Editor/VisualStudioInstallation.cs index 07c3e70..a927dd1 100644 --- a/Editor/VisualStudioInstallation.cs +++ b/Editor/VisualStudioInstallation.cs @@ -43,6 +43,10 @@ namespace Microsoft.Unity.VisualStudio.Editor // C# language version support for Visual Studio private static VersionPair[] WindowsVersionTable = { + // VisualStudio 2022 + new VersionPair(17,4, /* => */ 11,0), + new VersionPair(17,0, /* => */ 10,0), + // VisualStudio 2019 new VersionPair(16,8, /* => */ 9,0), new VersionPair(16,0, /* => */ 8,0), @@ -57,6 +61,10 @@ namespace Microsoft.Unity.VisualStudio.Editor // C# language version support for Visual Studio for Mac private static VersionPair[] OSXVersionTable = { + // VisualStudio for Mac 2022 + new VersionPair(17,4, /* => */ 11,0), + new VersionPair(17,0, /* => */ 10,0), + // VisualStudio for Mac 8.x new VersionPair(8,8, /* => */ 9,0), new VersionPair(8,3, /* => */ 8,0), diff --git a/package.json b/package.json index c787c04..4e03b5e 100644 --- a/package.json +++ b/package.json @@ -2,24 +2,22 @@ "name": "com.unity.ide.visualstudio", "displayName": "Visual Studio Editor", "description": "Code editor integration for supporting Visual Studio as code editor for unity. Adds support for generating csproj files for intellisense purposes, auto discovery of installations, etc.", - "version": "2.0.16", + "version": "2.0.17", "unity": "2019.4", "unityRelease": "25f1", "dependencies": { "com.unity.test-framework": "1.1.9" }, "relatedPackages": { - "com.unity.ide.visualstudio.tests": "2.0.16" - }, - "_upm": { - "changelog": "Integration:\n\n- Prevent ADB Refresh while being in safe-mode with a URP project\n- Fixed an issue keeping the progress bar visible even after opening a script with Visual Studio." + "com.unity.ide.visualstudio.tests": "2.0.17" }, "upmCi": { - "footprint": "3d5e14bed71dd5b89e088160c170e814d0058248" + "footprint": "95a297ed65f40d1df2fe8239f87deab3217a10b0" }, + "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.ide.visualstudio@2.0/manual/index.html", "repository": { "url": "https://github.cds.internal.unity3d.com/unity/com.unity.ide.visualstudio.git", "type": "git", - "revision": "c01855ef6461b821ab0226135e96a4ee86de96be" + "revision": "1f126893bfb18ea9661fb15771613e841467073c" } }