com.unity.ide.visualstudio@2.0.20

## [2.0.20] - 2023-06-27

Integration:

- Internal API refactoring.

## [2.0.19] - 2023-06-14

Integration:

- Add support for Visual Studio Code.

Project generation:

- Add support for Sdk Style poject generation.
- Fix an issue related to missing properties with 2021.3.
This commit is contained in:
Unity Technologies
2023-06-27 00:00:00 +00:00
parent ebe8973267
commit d1e4dd05ad
24 changed files with 1390 additions and 625 deletions

View File

@@ -1,5 +1,25 @@
# Code Editor Package for Visual Studio
## [2.0.20] - 2023-06-27
Integration:
- Internal API refactoring.
## [2.0.19] - 2023-06-14
Integration:
- Add support for Visual Studio Code.
Project generation:
- Add support for Sdk Style poject generation.
- Fix an issue related to missing properties with 2021.3.
## [2.0.18] - 2023-03-17
Integration:
@@ -10,7 +30,6 @@ Project generation:
- Add extra compiler options for analyzers and source generators.
## [2.0.17] - 2022-12-06
Integration:
@@ -24,7 +43,6 @@ Project generation:
- Update supported C# versions.
- Performance improvements.
## [2.0.16] - 2022-06-08
Integration:

View File

@@ -23,7 +23,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
internal static void GenerateSolutionWith(VisualStudioEditor vse, string installationPath)
{
if (vse != null && vse.TryGetVisualStudioInstallationForPath(installationPath, searchInstallations: true, out var vsi))
if (vse != null && vse.TryGetVisualStudioInstallationForPath(installationPath, lookupDiscoveredInstallations: true, out var vsi))
{
Log($"Using {GetInstallationDetails(vsi)}");
vse.SyncAll();

View File

@@ -3,162 +3,52 @@
* 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.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Linq;
using UnityEngine;
using System.IO;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal static class Discovery
{
internal const string ManagedWorkload = "Microsoft.VisualStudio.Workload.ManagedGame";
internal static string _vsWherePath;
public static void FindVSWhere()
{
_vsWherePath = FileUtility.GetPackageAssetFullPath("Editor", "VSWhere", "vswhere.exe");
}
public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations()
{
if (VisualStudioEditor.IsWindows)
{
foreach (var installation in QueryVsWhere())
foreach (var installation in VisualStudioForWindowsInstallation.GetVisualStudioInstallations())
yield return installation;
}
if (VisualStudioEditor.IsOSX)
{
var candidates = Directory.EnumerateDirectories("/Applications", "*.app");
foreach (var candidate in candidates)
{
if (TryDiscoverInstallation(candidate, out var installation))
foreach (var installation in VisualStudioForMacInstallation.GetVisualStudioInstallations())
yield return installation;
}
}
}
private static bool IsCandidateForDiscovery(string path)
{
if (File.Exists(path) && VisualStudioEditor.IsWindows && Regex.IsMatch(path, "devenv.exe$", RegexOptions.IgnoreCase))
return true;
if (Directory.Exists(path) && VisualStudioEditor.IsOSX && Regex.IsMatch(path, "Visual\\s?Studio(?!.*Code.*).*.app$", RegexOptions.IgnoreCase))
return true;
return false;
foreach (var installation in VisualStudioCodeInstallation.GetVisualStudioInstallations())
yield return installation;
}
public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation)
{
installation = null;
if (string.IsNullOrEmpty(editorPath))
return false;
if (!IsCandidateForDiscovery(editorPath))
return false;
// On windows we use the executable directly, so we can query extra information
var fvi = editorPath;
// On Mac we use the .app folder, so we need to access to main assembly
if (VisualStudioEditor.IsOSX)
try
{
fvi = Path.Combine(editorPath, "Contents/Resources/lib/monodevelop/bin/VisualStudio.exe");
if (VisualStudioForWindowsInstallation.TryDiscoverInstallation(editorPath, out installation))
return true;
if (!File.Exists(fvi))
fvi = Path.Combine(editorPath, "Contents/MonoBundle/VisualStudio.exe");
if (VisualStudioForMacInstallation.TryDiscoverInstallation(editorPath, out installation))
return true;
if (!File.Exists(fvi))
fvi = Path.Combine(editorPath, "Contents/MonoBundle/VisualStudio.dll");
}
if (!File.Exists(fvi))
return false;
// VS preview are not using the isPrerelease flag so far
// On Windows FileDescription contains "Preview", but not on Mac
var vi = FileVersionInfo.GetVersionInfo(fvi);
var version = new Version(vi.ProductVersion);
var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview");
installation = new VisualStudioInstallation()
{
IsPrerelease = isPrerelease,
Name = $"{vi.FileDescription}{(isPrerelease && VisualStudioEditor.IsOSX ? " Preview" : string.Empty)} [{version.ToString(3)}]",
Path = editorPath,
Version = version
};
if (VisualStudioCodeInstallation.TryDiscoverInstallation(editorPath, out installation))
return true;
}
#region VsWhere Json Schema
#pragma warning disable CS0649
[Serializable]
internal class VsWhereResult
catch (IOException)
{
public VsWhereEntry[] entries;
installation = null;
}
public static VsWhereResult FromJson(string json)
return false;
}
public static void Initialize()
{
return JsonUtility.FromJson<VsWhereResult>("{ \"" + nameof(VsWhereResult.entries) + "\": " + json + " }");
}
public IEnumerable<VisualStudioInstallation> ToVisualStudioInstallations()
{
foreach (var entry in entries)
{
yield return new VisualStudioInstallation()
{
Name = $"{entry.displayName} [{entry.catalog.productDisplayVersion}]",
Path = entry.productPath,
IsPrerelease = entry.isPrerelease,
Version = Version.Parse(entry.catalog.buildVersion)
};
}
}
}
[Serializable]
internal class VsWhereEntry
{
public string displayName;
public bool isPrerelease;
public string productPath;
public VsWhereCatalog catalog;
}
[Serializable]
internal class VsWhereCatalog
{
public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0"
public string buildVersion;
}
#pragma warning restore CS3021
#endregion
private static IEnumerable<VisualStudioInstallation> QueryVsWhere()
{
var progpath = _vsWherePath;
if (string.IsNullOrWhiteSpace(progpath))
return Enumerable.Empty<VisualStudioInstallation>();
var result = ProcessRunner.StartAndWaitForExit(progpath, "-prerelease -format json -utf8");
if (!result.Success)
throw new Exception($"Failure while running vswhere: {result.Error}");
// Do not catch any JsonException here, this will be handled by the caller
return VsWhereResult
.FromJson(result.Output)
.ToVisualStudioInstallations();
VisualStudioForWindowsInstallation.Initialize();
VisualStudioForMacInstallation.Initialize();
VisualStudioCodeInstallation.Initialize();
}
}
}

View File

@@ -6,6 +6,7 @@
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Unity.VisualStudio.Editor
@@ -21,19 +22,34 @@ namespace Microsoft.Unity.VisualStudio.Editor
{
public const int DefaultTimeoutInMilliseconds = 300000;
public static ProcessStartInfo ProcessStartInfoFor(string filename, string arguments)
public static ProcessStartInfo ProcessStartInfoFor(string filename, string arguments, bool redirect = true, bool shell = false)
{
return new ProcessStartInfo
{
UseShellExecute = false,
UseShellExecute = shell,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardOutput = redirect,
RedirectStandardError = redirect,
FileName = filename,
Arguments = arguments
};
}
public static void Start(string filename, string arguments)
{
Start(ProcessStartInfoFor(filename, arguments, false));
}
public static void Start(ProcessStartInfo processStartInfo)
{
var process = new Process { StartInfo = processStartInfo };
using (process)
{
process.Start();
}
}
public static ProcessRunnerResult StartAndWaitForExit(string filename, string arguments, int timeoutms = DefaultTimeoutInMilliseconds, Action<string> onOutputReceived = null)
{
return StartAndWaitForExit(ProcessStartInfoFor(filename, arguments), timeoutms, onOutputReceived);

View File

@@ -41,6 +41,12 @@ namespace Microsoft.Unity.VisualStudio.Editor
public string ProjectGenerationRootNamespace => EditorSettings.projectGenerationRootNamespace;
public ProjectGenerationFlag ProjectGenerationFlag
{
get { return ProjectGenerationFlagImpl; }
private set { ProjectGenerationFlagImpl = value;}
}
internal virtual ProjectGenerationFlag ProjectGenerationFlagImpl
{
get => m_ProjectGenerationFlag;
private set

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Unity Technologies.
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
using System.Text;
using UnityEditor.Compilation;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal class LegacyStyleProjectGeneration : ProjectGeneration
{
public LegacyStyleProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider, IFileIO fileIoProvider, IGUIDGenerator guidGenerator) : base(tempDirectory, assemblyNameProvider, fileIoProvider, guidGenerator)
{
}
public LegacyStyleProjectGeneration(string tempDirectory) : base(tempDirectory)
{
}
public LegacyStyleProjectGeneration()
{
}
internal override void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder)
{
headerBuilder = new StringBuilder();
//Header
headerBuilder.Append(@"<?xml version=""1.0"" encoding=""utf-8""?>").Append(k_WindowsNewline);
headerBuilder.Append($@"<Project ToolsVersion=""4.0"" DefaultTargets=""Build"" xmlns=""{MSBuildNamespaceUri}"">").Append(k_WindowsNewline);
headerBuilder.Append(@" <!-- Generated file, do not modify, your changes will be overwritten (use AssetPostprocessor.OnGeneratedCSProject) -->").Append(k_WindowsNewline);
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <LangVersion>").Append(properties.LangVersion).Append(@"</LangVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ProductVersion>10.0.20506</ProductVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <SchemaVersion>2.0</SchemaVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <RootNamespace>").Append(properties.RootNamespace).Append(@"</RootNamespace>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ProjectGuid>{").Append(properties.ProjectGuid).Append(@"}</ProjectGuid>").Append(k_WindowsNewline);
headerBuilder.Append(@" <OutputType>Library</OutputType>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AppDesignerFolder>Properties</AppDesignerFolder>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AssemblyName>").Append(properties.AssemblyName).Append(@"</AssemblyName>").Append(k_WindowsNewline);
// In the end, given we use NoConfig/NoStdLib (see below), hardcoding the target framework version with the legacy format will have no impact, even when targeting netstandard/net48 from Unity.
// And VSTU/Unity Game workload has a dependency towards net471 reference assemblies, so IDE will not complain that this specific SDK is not available.
// Unity already selected proper API surface through referenced DLLs for us.
headerBuilder.Append(@" <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <FileAlignment>512</FileAlignment>").Append(k_WindowsNewline);
headerBuilder.Append(@" <BaseDirectory>.</BaseDirectory>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
GetProjectHeaderConfigurations(properties, headerBuilder);
// Explicit references
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoConfig>true</NoConfig>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoStdLib>true</NoStdLib>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
GetProjectHeaderVstuFlavoring(properties, headerBuilder);
GetProjectHeaderAnalyzers(properties, headerBuilder);
}
internal override void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder)
{
// 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(@" <ProjectReference Include=""").Append(referenceName).Append(GetProjectExtension()).Append(@""">").Append(k_WindowsNewline);
projectBuilder.Append(" <Project>{").Append(ProjectGuid(referenceName)).Append("}</Project>").Append(k_WindowsNewline);
projectBuilder.Append(" <Name>").Append(referenceName).Append("</Name>").Append(k_WindowsNewline);
projectBuilder.Append(" </ProjectReference>").Append(k_WindowsNewline);
}
internal override void GetProjectFooter(StringBuilder footerBuilder)
{
footerBuilder.Append(string.Join(k_WindowsNewline,
@" <Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />",
@" <Target Name=""GenerateTargetFrameworkMonikerAttribute"" />",
@" <!-- To modify your build process, add your task inside one of the targets below and uncomment it.",
@" Other similar extension points exist, see Microsoft.Common.targets.",
@" <Target Name=""BeforeBuild"">",
@" </Target>",
@" <Target Name=""AfterBuild"">",
@" </Target>",
@" -->",
@"</Project>",
@""));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3070b6395b0f5f04faaab13164c6256d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -39,12 +39,14 @@ namespace Microsoft.Unity.VisualStudio.Editor
public class ProjectGeneration : IGenerator
{
// do not remove because of the Validation API, used in LegacyStyleProjectGeneration
public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003";
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";
internal const string k_WindowsNewline = "\r\n";
const string m_SolutionProjectEntryTemplate = @"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""{4}EndProject";
@@ -60,7 +62,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
HashSet<string> m_BuiltinSupportedExtensions = new HashSet<string>();
readonly string m_ProjectName;
readonly IAssemblyNameProvider m_AssemblyNameProvider;
internal readonly IAssemblyNameProvider m_AssemblyNameProvider;
readonly IFileIO m_FileIOProvider;
readonly IGUIDGenerator m_GUIDGenerator;
bool m_ShouldGenerateAll;
@@ -106,9 +108,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
SetupProjectSupportedExtensions();
// See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/
// We create a .vsconfig file to make sure our ManagedGame workload is installed
CreateVsConfigIfNotFound();
CreateExtraFiles(m_CurrentInstallation);
// Don't sync if we haven't synced before
var affected = affectedFiles as ICollection<string> ?? affectedFiles.ToArray();
@@ -148,26 +148,9 @@ namespace Microsoft.Unity.VisualStudio.Editor
}
}
private void CreateVsConfigIfNotFound()
private void CreateExtraFiles(IVisualStudioInstallation installation)
{
try
{
var vsConfigFile = VsConfigFile();
if (m_FileIOProvider.Exists(vsConfigFile))
return;
var content = $@"{{
""version"": ""1.0"",
""components"": [
""{Discovery.ManagedWorkload}""
]
}}
";
m_FileIOProvider.WriteAllText(vsConfigFile, content);
}
catch (IOException)
{
}
installation?.CreateExtraFiles(ProjectDirectory);
}
private bool HasFilesBeenModified(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles)
@@ -183,7 +166,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
private void RefreshCurrentInstallation()
{
var editor = CodeEditor.CurrentEditor as VisualStudioEditor;
editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, searchInstallations: true, out m_CurrentInstallation);
editor?.TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, lookupDiscoveredInstallations: true, out m_CurrentInstallation);
}
static ProfilerMarker solutionSyncMarker = new ProfilerMarker("SolutionSynchronizerSync");
@@ -199,7 +182,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
// See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/
// We create a .vsconfig file to make sure our ManagedGame workload is installed
CreateVsConfigIfNotFound();
CreateExtraFiles(m_CurrentInstallation);
var externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles();
@@ -366,7 +349,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
stringBuilders[assemblyName] = projectBuilder;
}
IncludeAsset(projectBuilder, "None", asset);
IncludeAsset(projectBuilder, IncludeAssetTag.None, asset);
}
}
@@ -378,7 +361,13 @@ namespace Microsoft.Unity.VisualStudio.Editor
return result;
}
private void IncludeAsset(StringBuilder builder, string tag, string asset)
internal enum IncludeAssetTag
{
Compile,
None
}
internal virtual void IncludeAsset(StringBuilder builder, IncludeAssetTag tag, string asset)
{
var filename = EscapedRelativePathFor(asset, out var packageInfo);
@@ -517,7 +506,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
if ("dll" != extensionWithoutDot)
{
IncludeAsset(projectBuilder, "Compile", file);
IncludeAsset(projectBuilder, IncludeAssetTag.Compile, file);
}
else
{
@@ -562,19 +551,13 @@ namespace Microsoft.Unity.VisualStudio.Editor
projectBuilder.Append(" <ItemGroup>").Append(k_WindowsNewline);
foreach (var reference in assembly.assemblyReferences.Where(i => i.sourceFiles.Any(ShouldFileBePartOfSolution)))
{
// 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(@" <ProjectReference Include=""").Append(referenceName).Append(GetProjectExtension()).Append(@""">").Append(k_WindowsNewline);
projectBuilder.Append(" <Project>{").Append(ProjectGuid(referenceName)).Append("}</Project>").Append(k_WindowsNewline);
projectBuilder.Append(" <Name>").Append(referenceName).Append("</Name>").Append(k_WindowsNewline);
projectBuilder.Append(" </ProjectReference>").Append(k_WindowsNewline);
AppendProjectReference(assembly, reference, projectBuilder);
}
projectBuilder.Append(@" </ItemGroup>").Append(k_WindowsNewline);
}
projectBuilder.Append(GetProjectFooter());
GetProjectFooter(projectBuilder);
return projectBuilder.ToString();
}
@@ -594,6 +577,10 @@ namespace Microsoft.Unity.VisualStudio.Editor
return SecurityElement.Escape(s);
}
internal virtual void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder)
{
}
private void AppendReference(string fullReference, StringBuilder projectBuilder)
{
var escapedFullPath = EscapedRelativePathFor(fullReference, out _);
@@ -614,11 +601,6 @@ namespace Microsoft.Unity.VisualStudio.Editor
return Path.Combine(ProjectDirectory.NormalizePathSeparators(), $"{InvalidCharactersRegexPattern.Replace(m_ProjectName, "_")}.sln");
}
internal string VsConfigFile()
{
return Path.Combine(ProjectDirectory.NormalizePathSeparators(), ".vsconfig");
}
internal string GetLangVersion(Assembly assembly)
{
var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version.
@@ -674,16 +656,25 @@ namespace Microsoft.Unity.VisualStudio.Editor
var additionalFilePaths = new List<string>();
var rulesetPath = string.Empty;
var analyzerConfigPath = string.Empty;
var compilerOptions = assembly.compilerOptions;
#if UNITY_2020_2_OR_NEWER
// Analyzers + ruleset provided by Unity
analyzers.AddRange(assembly.compilerOptions.RoslynAnalyzerDllPaths);
rulesetPath = assembly.compilerOptions.RoslynAnalyzerRulesetPath;
analyzers.AddRange(compilerOptions.RoslynAnalyzerDllPaths);
rulesetPath = compilerOptions.RoslynAnalyzerRulesetPath;
#endif
#if UNITY_2021_3_OR_NEWER && !UNITY_2022_1 // we have support in 2021.3, 2022.2 but without a backport in 2022.1
additionalFilePaths.AddRange(assembly.compilerOptions.RoslynAdditionalFilePaths);
analyzerConfigPath = assembly.compilerOptions.AnalyzerConfigPath;
// We have support in 2021.3, 2022.2 but without a backport in 2022.1
#if UNITY_2021_3
// Unfortunately those properties were introduced in a patch release of 2021.3, so not found in 2021.3.2f1 for example
var scoType = compilerOptions.GetType();
var afpProperty = scoType.GetProperty("RoslynAdditionalFilePaths");
var acpProperty = scoType.GetProperty("AnalyzerConfigPath");
additionalFilePaths.AddRange(afpProperty?.GetValue(compilerOptions) as string[] ?? Array.Empty<string>());
analyzerConfigPath = acpProperty?.GetValue(compilerOptions) as string ?? analyzerConfigPath;
#elif UNITY_2022_2_OR_NEWER
additionalFilePaths.AddRange(compilerOptions.RoslynAdditionalFilePaths);
analyzerConfigPath = compilerOptions.AnalyzerConfigPath;
#endif
// Analyzers and additional files provided by csc.rsp
@@ -765,30 +756,13 @@ namespace Microsoft.Unity.VisualStudio.Editor
return ProjectType.Game;
}
private void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder)
internal virtual void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder)
{
headerBuilder = new StringBuilder();
headerBuilder = default;
}
//Header
headerBuilder.Append(@"<?xml version=""1.0"" encoding=""utf-8""?>").Append(k_WindowsNewline);
headerBuilder.Append(@"<Project ToolsVersion=""4.0"" DefaultTargets=""Build"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">").Append(k_WindowsNewline);
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <LangVersion>").Append(properties.LangVersion).Append(@"</LangVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ProductVersion>10.0.20506</ProductVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <SchemaVersion>2.0</SchemaVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <RootNamespace>").Append(properties.RootNamespace).Append(@"</RootNamespace>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ProjectGuid>{").Append(properties.ProjectGuid).Append(@"}</ProjectGuid>").Append(k_WindowsNewline);
headerBuilder.Append(@" <OutputType>Library</OutputType>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AppDesignerFolder>Properties</AppDesignerFolder>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AssemblyName>").Append(properties.AssemblyName).Append(@"</AssemblyName>").Append(k_WindowsNewline);
headerBuilder.Append(@" <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <FileAlignment>512</FileAlignment>").Append(k_WindowsNewline);
headerBuilder.Append(@" <BaseDirectory>.</BaseDirectory>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
internal static void GetProjectHeaderConfigurations(ProjectProperties properties, StringBuilder headerBuilder)
{
headerBuilder.Append(@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "">").Append(k_WindowsNewline);
headerBuilder.Append(@" <DebugSymbols>true</DebugSymbols>").Append(k_WindowsNewline);
headerBuilder.Append(@" <DebugType>full</DebugType>").Append(k_WindowsNewline);
@@ -809,26 +783,10 @@ namespace Microsoft.Unity.VisualStudio.Editor
headerBuilder.Append(@" <NoWarn>0169</NoWarn>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AllowUnsafeBlocks>").Append(properties.Unsafe).Append(@"</AllowUnsafeBlocks>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
}
// Explicit references
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoConfig>true</NoConfig>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoStdLib>true</NoStdLib>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
// Flavoring
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <ProjectTypeGuids>{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityProjectGenerator>Package</UnityProjectGenerator>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityProjectGeneratorVersion>").Append(properties.FlavoringPackageVersion).Append(@"</UnityProjectGeneratorVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityProjectType>").Append(properties.FlavoringProjectType).Append(@"</UnityProjectType>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityBuildTarget>").Append(properties.FlavoringBuildTarget).Append(@"</UnityBuildTarget>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityVersion>").Append(properties.FlavoringUnityVersion).Append(@"</UnityVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
internal static void GetProjectHeaderAnalyzers(ProjectProperties properties, StringBuilder headerBuilder)
{
if (!string.IsNullOrEmpty(properties.RulesetPath))
{
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
@@ -864,20 +822,26 @@ namespace Microsoft.Unity.VisualStudio.Editor
}
}
private static string GetProjectFooter()
internal static void GetProjectHeaderVstuFlavoring(ProjectProperties properties, StringBuilder headerBuilder, bool includeProjectTypeGuids = true)
{
// Flavoring
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
if (includeProjectTypeGuids)
{
headerBuilder.Append(@" <ProjectTypeGuids>{E097FAD1-6243-4DAD-9C02-E9B9EFC3FFC1};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>").Append(k_WindowsNewline);
}
headerBuilder.Append(@" <UnityProjectGenerator>Package</UnityProjectGenerator>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityProjectGeneratorVersion>").Append(properties.FlavoringPackageVersion).Append(@"</UnityProjectGeneratorVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityProjectType>").Append(properties.FlavoringProjectType).Append(@"</UnityProjectType>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityBuildTarget>").Append(properties.FlavoringBuildTarget).Append(@"</UnityBuildTarget>").Append(k_WindowsNewline);
headerBuilder.Append(@" <UnityVersion>").Append(properties.FlavoringUnityVersion).Append(@"</UnityVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
}
internal virtual void GetProjectFooter(StringBuilder footerBuilder)
{
return string.Join(k_WindowsNewline,
@" <Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />",
@" <Target Name=""GenerateTargetFrameworkMonikerAttribute"" />",
@" <!-- To modify your build process, add your task inside one of the targets below and uncomment it.",
@" Other similar extension points exist, see Microsoft.Common.targets.",
@" <Target Name=""BeforeBuild"">",
@" </Target>",
@" <Target Name=""AfterBuild"">",
@" </Target>",
@" -->",
@"</Project>",
@"");
}
private static string GetSolutionText()
@@ -1024,7 +988,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
projectGuid);
}
private string EscapedRelativePathFor(string file, out UnityEditor.PackageManager.PackageInfo packageInfo)
internal string EscapedRelativePathFor(string file, out UnityEditor.PackageManager.PackageInfo packageInfo)
{
var projectDir = ProjectDirectory.NormalizePathSeparators();
file = file.NormalizePathSeparators();
@@ -1042,24 +1006,24 @@ namespace Microsoft.Unity.VisualStudio.Editor
return XmlFilename(path);
}
private static string SkipPathPrefix(string path, string prefix)
internal static string SkipPathPrefix(string path, string prefix)
{
if (path.StartsWith($"{prefix}{Path.DirectorySeparatorChar}") && (path.Length > prefix.Length))
return path.Substring(prefix.Length + 1);
return path;
}
static string GetProjectExtension()
internal static string GetProjectExtension()
{
return ".csproj";
}
private string ProjectGuid(string assemblyName)
internal string ProjectGuid(string assemblyName)
{
return m_GUIDGenerator.ProjectGuid(m_ProjectName, assemblyName);
}
private string ProjectGuid(Assembly assembly)
internal string ProjectGuid(Assembly assembly)
{
return ProjectGuid(m_AssemblyNameProvider.GetAssemblyName(assembly.outputPath, assembly.name));
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Unity Technologies.
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
using System.IO;
using System.Text;
using UnityEditor.Compilation;
using UnityEngine;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal class SdkStyleProjectGeneration : ProjectGeneration
{
internal class SdkStyleAssemblyNameProvider : AssemblyNameProvider
{
// disable PlayerGeneration with SdkStyle projects
internal override ProjectGenerationFlag ProjectGenerationFlagImpl => base.ProjectGenerationFlagImpl & ~ProjectGenerationFlag.PlayerAssemblies;
}
public SdkStyleProjectGeneration() : base(
Directory.GetParent(Application.dataPath)?.FullName,
new SdkStyleAssemblyNameProvider(),
new FileIOProvider(),
new GUIDProvider())
{
}
internal override void GetProjectHeader(ProjectProperties properties, out StringBuilder headerBuilder)
{
headerBuilder = new StringBuilder();
headerBuilder.Append(@"<Project ToolsVersion=""Current"" Sdk=""Microsoft.NET.Sdk"">").Append(k_WindowsNewline);
headerBuilder.Append(@" <!-- Generated file, do not modify, your changes will be overwritten (use AssetPostprocessor.OnGeneratedCSProject) -->").Append(k_WindowsNewline);
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <GenerateAssemblyInfo>false</GenerateAssemblyInfo>").Append(k_WindowsNewline);
headerBuilder.Append(@" <EnableDefaultItems>false</EnableDefaultItems>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>").Append(k_WindowsNewline);
headerBuilder.Append(@" <LangVersion>").Append(properties.LangVersion).Append(@"</LangVersion>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Configurations>Debug;Release</Configurations>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>").Append(k_WindowsNewline);
headerBuilder.Append(@" <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>").Append(k_WindowsNewline);
headerBuilder.Append(@" <RootNamespace>").Append(properties.RootNamespace).Append(@"</RootNamespace>").Append(k_WindowsNewline);
headerBuilder.Append(@" <OutputType>Library</OutputType>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AppDesignerFolder>Properties</AppDesignerFolder>").Append(k_WindowsNewline);
headerBuilder.Append(@" <AssemblyName>").Append(properties.AssemblyName).Append(@"</AssemblyName>").Append(k_WindowsNewline);
// In the end, given we use NoConfig/NoStdLib (see below), hardcoding the target framework version will have no impact, even when targeting netstandard/net48 from Unity.
// But with SDK style we use netstandard2.0 (net471 for legacy), so 3rd party tools will not fail to work when .NETFW reference assemblies are not installed.
// Unity already selected proper API surface through referenced DLLs for us.
headerBuilder.Append(@" <TargetFramework>netstandard2.0</TargetFramework>").Append(k_WindowsNewline);
headerBuilder.Append(@" <BaseDirectory>.</BaseDirectory>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
GetProjectHeaderConfigurations(properties, headerBuilder);
// Explicit references
headerBuilder.Append(@" <PropertyGroup>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoStandardLibraries>true</NoStandardLibraries>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoStdLib>true</NoStdLib>").Append(k_WindowsNewline);
headerBuilder.Append(@" <NoConfig>true</NoConfig>").Append(k_WindowsNewline);
headerBuilder.Append(@" <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>").Append(k_WindowsNewline);
headerBuilder.Append(@" <MSBuildWarningsAsMessages>MSB3277</MSBuildWarningsAsMessages>").Append(k_WindowsNewline);
headerBuilder.Append(@" </PropertyGroup>").Append(k_WindowsNewline);
GetProjectHeaderVstuFlavoring(properties, headerBuilder, false);
GetProjectHeaderAnalyzers(properties, headerBuilder);
}
internal override void AppendProjectReference(Assembly assembly, Assembly reference, StringBuilder projectBuilder)
{
// 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(@" <ProjectReference Include=""").Append(referenceName).Append(GetProjectExtension()).Append(@""" />").Append(k_WindowsNewline);
}
internal override void GetProjectFooter(StringBuilder footerBuilder)
{
footerBuilder.Append("</Project>").Append(k_WindowsNewline);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8aed47cdce10fd4cae32fefa2c34f8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,345 @@
/*---------------------------------------------------------------------------------------------
* 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using IOPath = System.IO.Path;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal class VisualStudioCodeInstallation : VisualStudioInstallation
{
private static readonly IGenerator _generator = new SdkStyleProjectGeneration();
public override bool SupportsAnalyzers
{
get
{
return true;
}
}
public override Version LatestLanguageVersionSupported
{
get
{
return new Version(11, 0);
}
}
private string GetExtensionPath()
{
var vscode = IsPrerelease ? ".vscode-insiders" : ".vscode";
var extensionsPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), vscode, "extensions");
if (!Directory.Exists(extensionsPath))
return null;
return Directory
.EnumerateDirectories(extensionsPath, "visualstudiotoolsforunity.vstuc*") // publisherid.extensionid
.OrderByDescending(n => n)
.FirstOrDefault();
}
public override string[] GetAnalyzers()
{
var vstuPath = GetExtensionPath();
if (string.IsNullOrEmpty(vstuPath))
return Array.Empty<string>();
return GetAnalyzers(vstuPath); }
public override IGenerator ProjectGenerator
{
get
{
return _generator;
}
}
private static bool IsCandidateForDiscovery(string path)
{
if (VisualStudioEditor.IsOSX)
return Directory.Exists(path) && Regex.IsMatch(path, ".*Code.*.app$", RegexOptions.IgnoreCase);
if (VisualStudioEditor.IsWindows)
return File.Exists(path) && Regex.IsMatch(path, ".*Code.*.exe$", RegexOptions.IgnoreCase);
return File.Exists(path) && path.EndsWith("code", StringComparison.OrdinalIgnoreCase);
}
[Serializable]
internal class VisualStudioCodeManifest
{
public string name;
public string version;
}
public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation)
{
installation = null;
if (string.IsNullOrEmpty(editorPath))
return false;
if (!IsCandidateForDiscovery(editorPath))
return false;
Version version = null;
var isPrerelease = false;
try
{
var manifestBase = GetRealPath(editorPath);
if (VisualStudioEditor.IsWindows) // on Windows, editorPath is a file, resources as subdirectory
manifestBase = IOPath.GetDirectoryName(manifestBase);
else if (VisualStudioEditor.IsOSX) // on Mac, editorPath is a directory
manifestBase = IOPath.Combine(manifestBase, "Contents");
else // on Linux, editorPath is a file, in a bin sub-directory
manifestBase = Directory.GetParent(manifestBase)?.Parent?.FullName;
if (manifestBase == null)
return false;
var manifestFullPath = IOPath.Combine(manifestBase, @"resources", "app", "package.json");
if (File.Exists(manifestFullPath))
{
var manifest = JsonUtility.FromJson<VisualStudioCodeManifest>(File.ReadAllText(manifestFullPath));
Version.TryParse(manifest.version.Split('-').First(), out version);
isPrerelease = manifest.version.ToLower().Contains("insider");
}
}
catch (Exception)
{
// do not fail if we are not able to retrieve the exact version number
}
isPrerelease = isPrerelease || editorPath.ToLower().Contains("insider");
installation = new VisualStudioCodeInstallation()
{
IsPrerelease = isPrerelease,
Name = "Visual Studio Code" + (isPrerelease ? " - Insider" : string.Empty) + (version != null ? $" [{version.ToString(3)}]" : string.Empty),
Path = editorPath,
Version = version ?? new Version()
};
return true;
}
public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations()
{
var candidates = new List<string>();
if (VisualStudioEditor.IsWindows)
{
var localAppPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs");
var programFiles = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles));
foreach (var basePath in new[] {localAppPath, programFiles})
{
candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code", "Code.exe"));
candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code Insiders", "Code - Insiders.exe"));
}
}
else if (VisualStudioEditor.IsOSX)
{
var appPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles));
candidates.AddRange(Directory.EnumerateDirectories(appPath, "Visual Studio Code*.app"));
}
else
{
candidates.Add("/usr/bin/code");
candidates.Add("/bin/code");
candidates.Add("/usr/local/bin/code");
}
foreach (var candidate in candidates)
{
if (TryDiscoverInstallation(candidate, out var installation))
yield return installation;
}
}
#if UNITY_EDITOR_LINUX
[DllImport ("libc")]
private static extern int readlink(string path, byte[] buffer, int buflen);
internal static string GetRealPath(string path)
{
byte[] buf = new byte[512];
int ret = readlink(path, buf, buf.Length);
if (ret == -1) return path;
char[] cbuf = new char[512];
int chars = System.Text.Encoding.Default.GetChars(buf, 0, ret, cbuf, 0);
return new String(cbuf, 0, chars);
}
#else
internal static string GetRealPath(string path)
{
return path;
}
#endif
public override void CreateExtraFiles(string projectDirectory)
{
try
{
// see https://tattoocoder.com/recommending-vscode-extensions-within-your-open-source-projects/
var vscodeDirectory = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vscode");
Directory.CreateDirectory(vscodeDirectory);
CreateRecommendedExtensionsFile(vscodeDirectory);
CreateSettingsFile(vscodeDirectory);
CreateLaunchFile(vscodeDirectory);
}
catch (IOException)
{
}
}
private static void CreateLaunchFile(string vscodeDirectory)
{
var launchFile = IOPath.Combine(vscodeDirectory, "launch.json");
if (File.Exists(launchFile))
return;
const string content = @"{
""version"": ""0.2.0"",
""configurations"": [
{
""name"": ""Attach to Unity"",
""type"": ""vstuc"",
""request"": ""attach"",
}
]
}";
File.WriteAllText(launchFile, content);
}
private static void CreateSettingsFile(string vscodeDirectory)
{
var settingsFile = IOPath.Combine(vscodeDirectory, "settings.json");
if (File.Exists(settingsFile))
return;
const string content = @"{
""files.exclude"":
{
""**/.DS_Store"":true,
""**/.git"":true,
""**/.vs"":true,
""**/.gitmodules"":true,
""**/.vsconfig"":true,
""**/*.booproj"":true,
""**/*.pidb"":true,
""**/*.suo"":true,
""**/*.user"":true,
""**/*.userprefs"":true,
""**/*.unityproj"":true,
""**/*.dll"":true,
""**/*.exe"":true,
""**/*.pdf"":true,
""**/*.mid"":true,
""**/*.midi"":true,
""**/*.wav"":true,
""**/*.gif"":true,
""**/*.ico"":true,
""**/*.jpg"":true,
""**/*.jpeg"":true,
""**/*.png"":true,
""**/*.psd"":true,
""**/*.tga"":true,
""**/*.tif"":true,
""**/*.tiff"":true,
""**/*.3ds"":true,
""**/*.3DS"":true,
""**/*.fbx"":true,
""**/*.FBX"":true,
""**/*.lxo"":true,
""**/*.LXO"":true,
""**/*.ma"":true,
""**/*.MA"":true,
""**/*.obj"":true,
""**/*.OBJ"":true,
""**/*.asset"":true,
""**/*.cubemap"":true,
""**/*.flare"":true,
""**/*.mat"":true,
""**/*.meta"":true,
""**/*.prefab"":true,
""**/*.unity"":true,
""build/"":true,
""Build/"":true,
""Library/"":true,
""library/"":true,
""obj/"":true,
""Obj/"":true,
""Logs/"":true,
""logs/"":true,
""ProjectSettings/"":true,
""UserSettings/"":true,
""temp/"":true,
""Temp/"":true
},
""omnisharp.enableRoslynAnalyzers"": true
}";
File.WriteAllText(settingsFile, content);
}
private static void CreateRecommendedExtensionsFile(string vscodeDirectory)
{
var extensionFile = IOPath.Combine(vscodeDirectory, "extensions.json");
if (File.Exists(extensionFile))
return;
const string content = @"{
""recommendations"": [
""visualstudiotoolsforunity.vstuc""
]
}
";
File.WriteAllText(extensionFile, content);
}
public override bool Open(string path, int line, int column, string solution)
{
line = Math.Max(1, line);
column = Math.Max(0, column);
var directory = IOPath.GetDirectoryName(solution);
var application = Path;
ProcessRunner.Start(string.IsNullOrEmpty(path) ?
ProcessStartInfoFor(application, $"\"{directory}\"") :
ProcessStartInfoFor(application, $"\"{directory}\" -g \"{path}\":{line}:{column}"));
return true;
}
private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments)
{
if (!VisualStudioEditor.IsOSX)
return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect: false);
// wrap with built-in OSX open feature
arguments = $"-n \"{application}\" --args {arguments}";
application = "open";
return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect:false, shell: true);
}
public static void Initialize()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1be5c96ff30e6ec40876f28fd9ab7e24
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -4,15 +4,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
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")]
@@ -26,39 +24,53 @@ namespace Microsoft.Unity.VisualStudio.Editor
internal static bool IsOSX => Application.platform == RuntimePlatform.OSXEditor;
internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == FileUtility.WinSeparator && Environment.NewLine == "\r\n";
CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations.Result
.Select(i => i.ToCodeEditorInstallation())
CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations
.Result
.Values
.Select(v => v.ToCodeEditorInstallation())
.ToArray();
private static readonly AsyncOperation<IVisualStudioInstallation[]> _discoverInstallations;
private readonly IGenerator _generator = new ProjectGeneration();
private static readonly AsyncOperation<Dictionary<string, IVisualStudioInstallation>> _discoverInstallations;
static VisualStudioEditor()
{
if (!UnityInstallation.IsMainUnityEditorProcess)
return;
if (IsWindows)
Discovery.FindVSWhere();
Discovery.Initialize();
CodeEditor.Register(new VisualStudioEditor());
_discoverInstallations = AsyncOperation<IVisualStudioInstallation[]>.Run(DiscoverInstallations);
_discoverInstallations = AsyncOperation<Dictionary<string, IVisualStudioInstallation>>.Run(DiscoverInstallations);
}
private static IVisualStudioInstallation[] DiscoverInstallations()
#if UNITY_2019_4_OR_NEWER && !UNITY_2020
[InitializeOnLoadMethod]
static void LegacyVisualStudioCodePackageDisabler()
{
// disable legacy Visual Studio Code packages
var editor = CodeEditor.Editor.GetCodeEditorForPath("code.cmd");
if (editor == null)
return;
if (editor is VisualStudioEditor)
return;
CodeEditor.Unregister(editor);
}
#endif
private static Dictionary<string, IVisualStudioInstallation> DiscoverInstallations()
{
try
{
return Discovery
.GetVisualStudioInstallations()
.ToArray();
.ToDictionary(i => Path.GetFullPath(i.Path), i => i);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Error detecting Visual Studio installations: {ex}");
return Array.Empty<IVisualStudioInstallation>();
Debug.LogError($"Error detecting Visual Studio installations: {ex}");
return new Dictionary<string, IVisualStudioInstallation>();
}
}
@@ -68,36 +80,33 @@ namespace Microsoft.Unity.VisualStudio.Editor
// keeping it for now given it is public, so we need a major bump to remove it
public void CreateIfDoesntExist()
{
if (!_generator.HasSolutionBeenGenerated())
_generator.Sync();
if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
return;
var generator = installation.ProjectGenerator;
if (!generator.HasSolutionBeenGenerated())
generator.Sync();
}
public void Initialize(string editorInstallationPath)
{
}
internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool searchInstallations, out IVisualStudioInstallation installation)
internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool lookupDiscoveredInstallations, out IVisualStudioInstallation installation)
{
if (searchInstallations)
{
// lookup for well known installations
foreach (var candidate in _discoverInstallations.Result)
{
if (!string.Equals(Path.GetFullPath(editorPath), Path.GetFullPath(candidate.Path), StringComparison.OrdinalIgnoreCase))
continue;
editorPath = Path.GetFullPath(editorPath);
installation = candidate;
// lookup for well known installations
if (lookupDiscoveredInstallations && _discoverInstallations.Result.TryGetValue(editorPath, out installation))
return true;
}
}
return Discovery.TryDiscoverInstallation(editorPath, out installation);
}
public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
{
var result = TryGetVisualStudioInstallationForPath(editorPath, searchInstallations: false, out var vsi);
installation = vsi == null ? default : vsi.ToCodeEditorInstallation();
var result = TryGetVisualStudioInstallationForPath(editorPath, lookupDiscoveredInstallations: false, out var vsi);
installation = vsi?.ToCodeEditorInstallation() ?? default;
return result;
}
@@ -106,6 +115,9 @@ namespace Microsoft.Unity.VisualStudio.Editor
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
return;
var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly);
var style = new GUIStyle
@@ -119,41 +131,44 @@ namespace Microsoft.Unity.VisualStudio.Editor
EditorGUILayout.LabelField("Generate .csproj files for:");
EditorGUI.indentLevel++;
SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "");
SettingsButton(ProjectGenerationFlag.Local, "Local packages", "");
SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "");
SettingsButton(ProjectGenerationFlag.Git, "Git packages", "");
SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "");
SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "");
SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "");
SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'");
RegenerateProjectFiles();
SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", installation);
SettingsButton(ProjectGenerationFlag.Local, "Local packages", "", installation);
SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "", installation);
SettingsButton(ProjectGenerationFlag.Git, "Git packages", "", installation);
SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "", installation);
SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "", installation);
SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "", installation);
SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'", installation);
RegenerateProjectFiles(installation);
EditorGUI.indentLevel--;
}
void RegenerateProjectFiles()
private static void RegenerateProjectFiles(IVisualStudioInstallation installation)
{
var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] { }));
var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect());
rect.width = 252;
if (GUI.Button(rect, "Regenerate project files"))
{
_generator.Sync();
installation.ProjectGenerator.Sync();
}
}
void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip)
private static void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip, IVisualStudioInstallation installation)
{
var prevValue = _generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
var generator = installation.ProjectGenerator;
var prevValue = generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
if (newValue != prevValue)
{
_generator.AssemblyNameProvider.ToggleProjectGeneration(preference);
}
generator.AssemblyNameProvider.ToggleProjectGeneration(preference);
}
public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles)
{
_generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
{
installation.ProjectGenerator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
}
foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb"))
{
@@ -170,16 +185,19 @@ namespace Microsoft.Unity.VisualStudio.Editor
if (Symbols.IsPortableSymbolFile(pdbFile))
continue;
UnityEngine.Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format.");
Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format.");
}
}
public void SyncAll()
{
_generator.Sync();
if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
{
installation.ProjectGenerator.Sync();
}
}
bool IsSupportedPath(string path)
private static bool IsSupportedPath(string path, IGenerator generator)
{
// Path is empty with "Open C# Project", as we only want to open the solution without specific files
if (string.IsNullOrEmpty(path))
@@ -188,44 +206,27 @@ namespace Microsoft.Unity.VisualStudio.Editor
// cs, uxml, uss, shader, compute, cginc, hlsl, glslinc, template are part of Unity builtin extensions
// txt, xml, fnt, cd are -often- par of Unity user extensions
// asdmdef is mandatory included
if (_generator.IsSupportedFile(path))
return true;
return false;
}
private static void CheckCurrentEditorInstallation()
{
var editorPath = CodeEditor.CurrentEditorInstallation;
try
{
if (Discovery.TryDiscoverInstallation(editorPath, out _))
return;
}
catch (IOException)
{
}
UnityEngine.Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools.");
return generator.IsSupportedFile(path);
}
public bool OpenProject(string path, int line, int column)
{
CheckCurrentEditorInstallation();
var editorPath = CodeEditor.CurrentEditorInstallation;
if (!IsSupportedPath(path))
if (!Discovery.TryDiscoverInstallation(editorPath, out var installation)) {
Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools.");
return false;
}
var generator = installation.ProjectGenerator;
if (!IsSupportedPath(path, generator))
return false;
if (!IsProjectGeneratedFor(path, out var missingFlag))
UnityEngine.Debug.LogWarning($"You are trying to open {path} outside a generated project. This might cause problems with IntelliSense and debugging. To avoid this, you can change your .csproj preferences in Edit > Preferences > External Tools and enable {GetProjectGenerationFlagDescription(missingFlag)} generation.");
if (!IsProjectGeneratedFor(path, generator, out var missingFlag))
Debug.LogWarning($"You are trying to open {path} outside a generated project. This might cause problems with IntelliSense and debugging. To avoid this, you can change your .csproj preferences in Edit > Preferences > External Tools and enable {GetProjectGenerationFlagDescription(missingFlag)} generation.");
if (IsOSX)
return OpenOSXApp(path, line, column);
if (IsWindows)
return OpenWindowsApp(path, line);
return false;
var solution = GetOrGenerateSolutionFile(generator);
return installation.Open(path, line, column, solution);
}
private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag)
@@ -253,7 +254,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
}
}
private bool IsProjectGeneratedFor(string path, out ProjectGenerationFlag missingFlag)
private static bool IsProjectGeneratedFor(string path, IGenerator generator, out ProjectGenerationFlag missingFlag)
{
missingFlag = ProjectGenerationFlag.None;
@@ -266,9 +267,9 @@ namespace Microsoft.Unity.VisualStudio.Editor
return true;
// Even on windows, the package manager requires relative path + unix style separators for queries
var basePath = _generator.ProjectDirectory;
var relativePath = FileUtility
.NormalizeWindowsToUnix(path)
var basePath = generator.ProjectDirectory;
var relativePath = path
.NormalizeWindowsToUnix()
.Replace(basePath, string.Empty)
.Trim(FileUtility.UnixSeparator);
@@ -280,7 +281,7 @@ namespace Microsoft.Unity.VisualStudio.Editor
if (!Enum.TryParse<ProjectGenerationFlag>(source.ToString(), out var flag))
return true;
if (_generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag))
if (generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag))
return true;
// Return false if we found a source not flagged for generation
@@ -288,118 +289,10 @@ namespace Microsoft.Unity.VisualStudio.Editor
return false;
}
private enum COMIntegrationState
private static string GetOrGenerateSolutionFile(IGenerator generator)
{
Running,
DisplayProgressBar,
ClearProgressBar,
Exited
}
private bool OpenWindowsApp(string path, int line)
{
var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe");
if (string.IsNullOrWhiteSpace(progpath))
return false;
string absolutePath = "";
if (!string.IsNullOrWhiteSpace(path))
{
absolutePath = Path.GetFullPath(path);
}
// We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project
// So process the fullpath to make it compatible with VS
var solution = GetOrGenerateSolutionFile(path);
if (!string.IsNullOrWhiteSpace(solution))
{
solution = $"\"{solution}\"";
solution = solution.Replace("^", "^^");
}
var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}");
psi.StandardOutputEncoding = System.Text.Encoding.Unicode;
psi.StandardErrorEncoding = System.Text.Encoding.Unicode;
// inter thread communication
var messages = new BlockingCollection<COMIntegrationState>();
var asyncStart = AsyncOperation<ProcessRunnerResult>.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<COMIntegrationState> messages)
{
var displayingProgress = false;
COMIntegrationState state;
do
{
state = messages.Take();
switch (state)
{
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<COMIntegrationState> messages)
{
if (data == null)
return;
foreach (var cmd in ProgressBarCommands)
{
if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0)
messages.Add(cmd);
}
}
[DllImport("AppleEventIntegration")]
static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line);
bool OpenOSXApp(string path, int line, int column)
{
string absolutePath = "";
if (!string.IsNullOrWhiteSpace(path))
{
absolutePath = Path.GetFullPath(path);
}
var solution = GetOrGenerateSolutionFile(path);
return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line);
}
private string GetOrGenerateSolutionFile(string path)
{
_generator.Sync();
return _generator.SolutionFile();
generator.Sync();
return generator.SolutionFile();
}
}
}

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Unity.CodeEditor;
using IOPath = System.IO.Path;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal class VisualStudioForMacInstallation : VisualStudioInstallation
{
// C# language version support for Visual Studio for Mac
private static readonly 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),
new VersionPair(8,0, /* => */ 7,3),
};
private static readonly IGenerator _generator = new LegacyStyleProjectGeneration();
public override bool SupportsAnalyzers
{
get
{
return Version >= new Version(8, 3);
}
}
public override Version LatestLanguageVersionSupported
{
get
{
return GetLatestLanguageVersionSupported(OSXVersionTable);
}
}
private string GetExtensionPath()
{
const string addinName = "MonoDevelop.Unity";
const string addinAssembly = addinName + ".dll";
// user addins repository
var localAddins = IOPath.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
$"Library/Application Support/VisualStudio/${Version.Major}.0" + "/LocalInstall/Addins");
// In the user addins repository, the addins are suffixed by their versions, like `MonoDevelop.Unity.1.0`
// When installing another local user addin, MD will remove files inside the folder
// So we browse all VSTUM addins, and return the one with an addin assembly
if (Directory.Exists(localAddins))
{
foreach (var folder in Directory.GetDirectories(localAddins, addinName + "*", SearchOption.TopDirectoryOnly))
{
if (File.Exists(IOPath.Combine(folder, addinAssembly)))
return folder;
}
}
// Check in Visual Studio.app/
// In that case the name of the addin is used
var addinPath = IOPath.Combine(Path, $"Contents/Resources/lib/monodevelop/AddIns/{addinName}");
if (File.Exists(IOPath.Combine(addinPath, addinAssembly)))
return addinPath;
addinPath = IOPath.Combine(Path, $"Contents/MonoBundle/Addins/{addinName}");
if (File.Exists(IOPath.Combine(addinPath, addinAssembly)))
return addinPath;
return null;
}
public override string[] GetAnalyzers()
{
var vstuPath = GetExtensionPath();
if (string.IsNullOrEmpty(vstuPath))
return Array.Empty<string>();
return GetAnalyzers(vstuPath);
}
public override IGenerator ProjectGenerator
{
get
{
return _generator;
}
}
private static bool IsCandidateForDiscovery(string path)
{
return Directory.Exists(path) && VisualStudioEditor.IsOSX && Regex.IsMatch(path, "Visual\\s?Studio(?!.*Code.*).*.app$", RegexOptions.IgnoreCase);
}
public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation)
{
installation = null;
if (string.IsNullOrEmpty(editorPath))
return false;
if (!IsCandidateForDiscovery(editorPath))
return false;
// On Mac we use the .app folder, so we need to access to main assembly
var fvi = IOPath.Combine(editorPath, "Contents/Resources/lib/monodevelop/bin/VisualStudio.exe");
if (!File.Exists(fvi))
fvi = IOPath.Combine(editorPath, "Contents/MonoBundle/VisualStudio.exe");
if (!File.Exists(fvi))
fvi = IOPath.Combine(editorPath, "Contents/MonoBundle/VisualStudio.dll");
if (!File.Exists(fvi))
return false;
// VS preview are not using the isPrerelease flag so far
// On Windows FileDescription contains "Preview", but not on Mac
var vi = FileVersionInfo.GetVersionInfo(fvi);
var version = new Version(vi.ProductVersion);
var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview");
installation = new VisualStudioForMacInstallation()
{
IsPrerelease = isPrerelease,
Name = $"{vi.FileDescription}{(isPrerelease ? " Preview" : string.Empty)} [{version.ToString(3)}]",
Path = editorPath,
Version = version
};
return true;
}
public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations()
{
if (!VisualStudioEditor.IsOSX)
yield break;
var candidates = Directory.EnumerateDirectories("/Applications", "*.app");
foreach (var candidate in candidates)
{
if (TryDiscoverInstallation(candidate, out var installation))
yield return installation;
}
}
[DllImport("AppleEventIntegration")]
private static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line);
public override void CreateExtraFiles(string projectDirectory)
{
}
public override bool Open(string path, int line, int column, string solution)
{
string absolutePath = "";
if (!string.IsNullOrWhiteSpace(path))
{
absolutePath = IOPath.GetFullPath(path);
}
return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line);
}
public static void Initialize()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c64241ee5e302b478d7f2522bbaa4e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,363 @@
/*---------------------------------------------------------------------------------------------
* 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Win32;
using Unity.CodeEditor;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
using IOPath = System.IO.Path;
namespace Microsoft.Unity.VisualStudio.Editor
{
internal class VisualStudioForWindowsInstallation : VisualStudioInstallation
{
// C# language version support for Visual Studio
private static readonly 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),
// VisualStudio 2017
new VersionPair(15,7, /* => */ 7,3),
new VersionPair(15,5, /* => */ 7,2),
new VersionPair(15,3, /* => */ 7,1),
new VersionPair(15,0, /* => */ 7,0),
};
private static string _vsWherePath = null;
private static readonly IGenerator _generator = new LegacyStyleProjectGeneration();
public override bool SupportsAnalyzers
{
get
{
return Version >= new Version(16, 3);
}
}
public override Version LatestLanguageVersionSupported
{
get
{
return GetLatestLanguageVersionSupported(WindowsVersionTable);
}
}
private static string ReadRegistry(RegistryKey hive, string keyName, string valueName)
{
try
{
var unitykey = hive.OpenSubKey(keyName);
var result = (string)unitykey?.GetValue(valueName);
return result;
}
catch (Exception)
{
return null;
}
}
private string GetWindowsBridgeFromRegistry()
{
var keyName = $"Software\\Microsoft\\Microsoft Visual Studio {Version.Major}.0 Tools for Unity";
const string valueName = "UnityExtensionPath";
var bridge = ReadRegistry(Registry.CurrentUser, keyName, valueName);
if (string.IsNullOrEmpty(bridge))
bridge = ReadRegistry(Registry.LocalMachine, keyName, valueName);
return bridge;
}
private string GetExtensionPath()
{
const string extensionName = "Visual Studio Tools for Unity";
const string extensionAssembly = "SyntaxTree.VisualStudio.Unity.dll";
var vsDirectory = IOPath.GetDirectoryName(Path);
var vstuDirectory = IOPath.Combine(vsDirectory, "Extensions", "Microsoft", extensionName);
if (File.Exists(IOPath.Combine(vstuDirectory, extensionAssembly)))
return vstuDirectory;
return null;
}
public override string[] GetAnalyzers()
{
var vstuPath = GetExtensionPath();
if (string.IsNullOrEmpty(vstuPath))
return Array.Empty<string>();
var analyzers = GetAnalyzers(vstuPath);
if (analyzers?.Length > 0)
return analyzers;
var bridge = GetWindowsBridgeFromRegistry();
if (File.Exists(bridge))
return GetAnalyzers(IOPath.Combine(IOPath.GetDirectoryName(bridge), ".."));
return Array.Empty<string>();
}
public override IGenerator ProjectGenerator
{
get
{
return _generator;
}
}
private static bool IsCandidateForDiscovery(string path)
{
return File.Exists(path) && VisualStudioEditor.IsWindows && Regex.IsMatch(path, "devenv.exe$", RegexOptions.IgnoreCase);
}
public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation)
{
installation = null;
if (string.IsNullOrEmpty(editorPath))
return false;
if (!IsCandidateForDiscovery(editorPath))
return false;
// On windows we use the executable directly, so we can query extra information
if (!File.Exists(editorPath))
return false;
// VS preview are not using the isPrerelease flag so far
// On Windows FileDescription contains "Preview", but not on Mac
var vi = FileVersionInfo.GetVersionInfo(editorPath);
var version = new Version(vi.ProductVersion);
var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview");
installation = new VisualStudioForWindowsInstallation()
{
IsPrerelease = isPrerelease,
Name = $"{FormatProductName(vi.FileDescription)} [{version.ToString(3)}]",
Path = editorPath,
Version = version
};
return true;
}
public static string FormatProductName(string productName)
{
if (string.IsNullOrEmpty(productName))
return string.Empty;
return productName.Replace("Microsoft ", string.Empty);
}
public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations()
{
if (!VisualStudioEditor.IsWindows)
yield break;
foreach (var installation in QueryVsWhere())
yield return installation;
}
#region VsWhere Json Schema
#pragma warning disable CS0649
[Serializable]
internal class VsWhereResult
{
public VsWhereEntry[] entries;
public static VsWhereResult FromJson(string json)
{
return JsonUtility.FromJson<VsWhereResult>("{ \"" + nameof(VsWhereResult.entries) + "\": " + json + " }");
}
public IEnumerable<VisualStudioInstallation> ToVisualStudioInstallations()
{
foreach (var entry in entries)
{
yield return new VisualStudioForWindowsInstallation
{
Name = $"{FormatProductName(entry.displayName)} [{entry.catalog.productDisplayVersion}]",
Path = entry.productPath,
IsPrerelease = entry.isPrerelease,
Version = Version.Parse(entry.catalog.buildVersion)
};
}
}
}
[Serializable]
internal class VsWhereEntry
{
public string displayName;
public bool isPrerelease;
public string productPath;
public VsWhereCatalog catalog;
}
[Serializable]
internal class VsWhereCatalog
{
public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0"
public string buildVersion;
}
#pragma warning restore CS3021
#endregion
private static IEnumerable<VisualStudioInstallation> QueryVsWhere()
{
var progpath = _vsWherePath;
if (string.IsNullOrWhiteSpace(progpath))
return Enumerable.Empty<VisualStudioInstallation>();
var result = ProcessRunner.StartAndWaitForExit(progpath, "-prerelease -format json -utf8");
if (!result.Success)
throw new Exception($"Failure while running vswhere: {result.Error}");
// Do not catch any JsonException here, this will be handled by the caller
return VsWhereResult
.FromJson(result.Output)
.ToVisualStudioInstallations();
}
private enum COMIntegrationState
{
Running,
DisplayProgressBar,
ClearProgressBar,
Exited
}
public override void CreateExtraFiles(string projectDirectory)
{
// See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/
// We create a .vsconfig file to make sure our ManagedGame workload is installed
try
{
var vsConfigFile = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vsconfig");
if (File.Exists(vsConfigFile))
return;
const string content = @"{
""version"": ""1.0"",
""components"": [
""Microsoft.VisualStudio.Workload.ManagedGame""
]
}
";
File.WriteAllText(vsConfigFile, content);
}
catch (IOException)
{
}
}
public override bool Open(string path, int line, int column, string solution)
{
var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe");
if (string.IsNullOrWhiteSpace(progpath))
return false;
string absolutePath = "";
if (!string.IsNullOrWhiteSpace(path))
{
absolutePath = IOPath.GetFullPath(path);
}
// We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project
// So process the fullpath to make it compatible with VS
if (!string.IsNullOrWhiteSpace(solution))
{
solution = $"\"{solution}\"";
solution = solution.Replace("^", "^^");
}
var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}");
psi.StandardOutputEncoding = System.Text.Encoding.Unicode;
psi.StandardErrorEncoding = System.Text.Encoding.Unicode;
// inter thread communication
var messages = new BlockingCollection<COMIntegrationState>();
var asyncStart = AsyncOperation<ProcessRunnerResult>.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<COMIntegrationState> messages)
{
var displayingProgress = false;
COMIntegrationState state;
do
{
state = messages.Take();
switch (state)
{
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<COMIntegrationState> messages)
{
if (data == null)
return;
foreach (var cmd in ProgressBarCommands)
{
if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0)
messages.Add(cmd);
}
}
public static void Initialize()
{
if (VisualStudioEditor.IsWindows)
_vsWherePath = FileUtility.GetPackageAssetFullPath("Editor", "VSWhere", "vswhere.exe");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be7ef402265a7a549b2e43c11d1a22c5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
using System;
using System.IO;
using Microsoft.Win32;
using Unity.CodeEditor;
using IOPath = System.IO.Path;
@@ -17,72 +16,27 @@ namespace Microsoft.Unity.VisualStudio.Editor
Version LatestLanguageVersionSupported { get; }
string[] GetAnalyzers();
CodeEditor.Installation ToCodeEditorInstallation();
bool Open(string path, int line, int column, string solutionPath);
IGenerator ProjectGenerator { get; }
void CreateExtraFiles(string projectDirectory);
}
internal class VisualStudioInstallation : IVisualStudioInstallation
internal abstract class VisualStudioInstallation : IVisualStudioInstallation
{
public string Name { get; set; }
public string Path { get; set; }
public Version Version { get; set; }
public bool IsPrerelease { get; set; }
public bool SupportsAnalyzers
public abstract bool SupportsAnalyzers { get; }
public abstract Version LatestLanguageVersionSupported { get; }
public abstract string[] GetAnalyzers();
public abstract IGenerator ProjectGenerator { get; }
public abstract void CreateExtraFiles(string projectDirectory);
public abstract bool Open(string path, int line, int column, string solutionPath);
protected Version GetLatestLanguageVersionSupported(VersionPair[] versions)
{
get
{
if (VisualStudioEditor.IsWindows)
return Version >= new Version(16, 3);
if (VisualStudioEditor.IsOSX)
return Version >= new Version(8, 3);
return false;
}
}
// 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),
// VisualStudio 2017
new VersionPair(15,7, /* => */ 7,3),
new VersionPair(15,5, /* => */ 7,2),
new VersionPair(15,3, /* => */ 7,1),
new VersionPair(15,0, /* => */ 7,0),
};
// 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),
new VersionPair(8,0, /* => */ 7,3),
};
public Version LatestLanguageVersionSupported
{
get
{
VersionPair[] versions = null;
if (VisualStudioEditor.IsWindows)
versions = WindowsVersionTable;
if (VisualStudioEditor.IsOSX)
versions = OSXVersionTable;
if (versions != null)
{
foreach (var entry in versions)
@@ -92,90 +46,11 @@ namespace Microsoft.Unity.VisualStudio.Editor
}
}
// default to 7.0 given we support at least VS 2017
// default to 7.0
return new Version(7, 0);
}
}
private static string ReadRegistry(RegistryKey hive, string keyName, string valueName)
{
try
{
var unitykey = hive.OpenSubKey(keyName);
var result = (string)unitykey?.GetValue(valueName);
return result;
}
catch (Exception)
{
return null;
}
}
private string GetWindowsBridgeFromRegistry()
{
var keyName = $"Software\\Microsoft\\Microsoft Visual Studio {Version.Major}.0 Tools for Unity";
const string valueName = "UnityExtensionPath";
var bridge = ReadRegistry(Registry.CurrentUser, keyName, valueName);
if (string.IsNullOrEmpty(bridge))
bridge = ReadRegistry(Registry.LocalMachine, keyName, valueName);
return bridge;
}
// We only use this to find analyzers, we do not need to load this assembly anymore
private string GetExtensionPath()
{
if (VisualStudioEditor.IsWindows)
{
const string extensionName = "Visual Studio Tools for Unity";
const string extensionAssembly = "SyntaxTree.VisualStudio.Unity.dll";
var vsDirectory = IOPath.GetDirectoryName(Path);
var vstuDirectory = IOPath.Combine(vsDirectory, "Extensions", "Microsoft", extensionName);
if (File.Exists(IOPath.Combine(vstuDirectory, extensionAssembly)))
return vstuDirectory;
}
if (VisualStudioEditor.IsOSX)
{
const string addinName = "MonoDevelop.Unity";
const string addinAssembly = addinName + ".dll";
// user addins repository
var localAddins = IOPath.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
$"Library/Application Support/VisualStudio/${Version.Major}.0" + "/LocalInstall/Addins");
// In the user addins repository, the addins are suffixed by their versions, like `MonoDevelop.Unity.1.0`
// When installing another local user addin, MD will remove files inside the folder
// So we browse all VSTUM addins, and return the one with an addin assembly
if (Directory.Exists(localAddins))
{
foreach (var folder in Directory.GetDirectories(localAddins, addinName + "*", SearchOption.TopDirectoryOnly))
{
if (File.Exists(IOPath.Combine(folder, addinAssembly)))
return folder;
}
}
// Check in Visual Studio.app/
// In that case the name of the addin is used
var addinPath = IOPath.Combine(Path, $"Contents/Resources/lib/monodevelop/AddIns/{addinName}");
if (File.Exists(IOPath.Combine(addinPath, addinAssembly)))
return addinPath;
addinPath = IOPath.Combine(Path, $"Contents/MonoBundle/Addins/{addinName}");
if (File.Exists(IOPath.Combine(addinPath, addinAssembly)))
return addinPath;
}
return null;
}
private static string[] GetAnalyzers(string path)
protected static string[] GetAnalyzers(string path)
{
var analyzersDirectory = IOPath.GetFullPath(IOPath.Combine(path, "Analyzers"));
@@ -185,31 +60,6 @@ namespace Microsoft.Unity.VisualStudio.Editor
return Array.Empty<string>();
}
public string[] GetAnalyzers()
{
var vstuPath = GetExtensionPath();
if (string.IsNullOrEmpty(vstuPath))
return Array.Empty<string>();
if (VisualStudioEditor.IsOSX)
return GetAnalyzers(vstuPath);
if (VisualStudioEditor.IsWindows)
{
var analyzers = GetAnalyzers(vstuPath);
if (analyzers?.Length > 0)
return analyzers;
var bridge = GetWindowsBridgeFromRegistry();
if (File.Exists(bridge))
return GetAnalyzers(IOPath.Combine(IOPath.GetDirectoryName(bridge), ".."));
}
// Local assets
// return FileUtility.FindPackageAssetFullPath("Analyzers a:packages", ".Analyzers.dll");
return Array.Empty<string>();
}
public CodeEditor.Installation ToCodeEditorInstallation()
{
return new CodeEditor.Installation() { Name = Name, Path = Path };

View File

@@ -7,7 +7,7 @@
"Targets": "+",
"Files":
[
"Editor/Discovery.cs"
"Editor/VisualStudioForWindowsInstallation.cs"
],
"Patterns":
[

View File

@@ -2,8 +2,13 @@
"ErrorExceptions": [
{
"ValidationTest": "API Validation",
"ExceptionMessage": "Failed comparing against assemblies of previously promoted version of package. \nThis is most likely because the assemblies that were compared against were built with a different version of Unity. \nIf you are certain that there are no API changes warranting bumping the package version then you can add an exception for this error:\nRead more about this error and potential solutions at https://docs.unity3d.com/Packages/com.unity.package-validation-suite@latest/index.html?preview=1&subfolder=/manual/validation_exceptions.html#",
"PackageVersion": "2.0.11"
"ExceptionMessage": "Breaking changes require a new major version.",
"PackageVersion": "2.0.18"
},
{
"ValidationTest": "API Validation",
"ExceptionMessage": "Additions require a new minor or major version.",
"PackageVersion": "2.0.18"
}
],
"WarningExceptions": []

View File

@@ -2,25 +2,25 @@
"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.18",
"version": "2.0.20",
"unity": "2019.4",
"unityRelease": "25f1",
"dependencies": {
"com.unity.test-framework": "1.1.9"
},
"relatedPackages": {
"com.unity.ide.visualstudio.tests": "2.0.18"
"com.unity.ide.visualstudio.tests": "2.0.20"
},
"_upm": {
"changelog": "Integration:\n\n- Performance improvements with `EditorApplication.update` callbacks.\n \nProject generation:\n\n- Add extra compiler options for analyzers and source generators."
"changelog": "Integration:\n\n- Internal API refactoring."
},
"upmCi": {
"footprint": "1d7ac8985c088423201e27b93ccdc6292ff941c9"
"footprint": "7d769a8558c7768417b16fc2ac8477cf69234049"
},
"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": "9d3c07127cbe1916b8abbfd18f71fb8d9df8008c"
"revision": "b7bf23d23806ac75645bfa12acadcfc11468a383"
}
}