com.unity.ide.visualstudio@1.0.2

# Code Editor Package for Visual Studio

## [1.0.2] - 2019-01-01

### This is the first release of *Unity Package visualstudio_editor*.

Using the newly created api to integrate Visual Studio with Unity.
This commit is contained in:
Unity Technologies
2019-01-01 00:00:00 +00:00
commit f31b3f54a7
31 changed files with 1883 additions and 0 deletions

7
CHANGELOG.md Normal file
View File

@@ -0,0 +1,7 @@
# Code Editor Package for Visual Studio
## [1.0.2] - 2019-01-01
### This is the first release of *Unity Package visualstudio_editor*.
Using the newly created api to integrate Visual Studio with Unity.

7
CHANGELOG.md.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6022bdd5b2896a54f94123b812705321
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

6
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,6 @@
# Contributing
## All contributions are subject to the [Unity Contribution Agreement(UCA)](https://unity3d.com/legal/licenses/Unity_Contribution_Agreement)
By making a pull request, you are confirming agreement to the terms and conditions of the UCA, including that your Contributions are your original creation and that you have complete right and authority to make your Contributions.
## Once you have a change ready following these ground rules. Simply make a pull request

7
CONTRIBUTING.md.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cd78ce5b75b8be84d8b94cd99903161f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

4
Documentation~/README.md Normal file
View File

@@ -0,0 +1,4 @@
# Code Editor Package for Visual Studio
This package is not intended to be modified by users.
Nor does it provide any api intended to be included in user projects.

8
Editor.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4a707af59e3522d43afe34dfc4429b86
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 36422aa067e092e45b9820da2ee3e728
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 41b2d972bdac29e4a89ef08b3b52c248
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cb67edc1800c2ec4ba8dfb1cf0dfc84a
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

47
Editor/Discovery.cs Normal file
View File

@@ -0,0 +1,47 @@
using System;
using System.Linq;
using Unity.CodeEditor;
using UnityEngine;
namespace VisualStudioEditor
{
public interface IDiscovery {
CodeEditor.Installation[] PathCallback();
}
public class Discovery : IDiscovery {
internal static string VisualStudioVersionToNiceName(VisualStudioVersion version)
{
switch (version)
{
case VisualStudioVersion.Invalid: return "Invalid Version";
case VisualStudioVersion.VisualStudio2008: return "Visual Studio 2008";
case VisualStudioVersion.VisualStudio2010: return "Visual Studio 2010";
case VisualStudioVersion.VisualStudio2012: return "Visual Studio 2012";
case VisualStudioVersion.VisualStudio2013: return "Visual Studio 2013";
case VisualStudioVersion.VisualStudio2015: return "Visual Studio 2015";
case VisualStudioVersion.VisualStudio2017: return "Visual Studio 2017";
case VisualStudioVersion.VisualStudio2019: return "Visual Studio 2019";
default:
throw new ArgumentOutOfRangeException(nameof(version), version, null);
}
}
public CodeEditor.Installation[] PathCallback()
{
try
{
return VSEditor.GetInstalledVisualStudios().Select(pair => new CodeEditor.Installation
{
Path = pair.Value[0],
Name = VisualStudioVersionToNiceName(pair.Key)
}).ToArray();
}
catch (Exception ex)
{
Debug.Log($"Error detecting Visual Studio installations: {ex.Message}{Environment.NewLine}{ex.StackTrace}");
return new CodeEditor.Installation[0];
}
}
}
}

11
Editor/Discovery.cs.meta Normal file
View File

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

845
Editor/ProjectGeneration.cs Normal file
View File

@@ -0,0 +1,845 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEditor.PackageManager;
using UnityEditor.Scripting.Compilers;
using UnityEditorInternal;
using UnityEngine;
namespace VisualStudioEditor
{
public interface IGenerator {
bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles);
void Sync();
bool HasSolutionBeenGenerated();
string SolutionFile();
string ProjectDirectory { get; }
}
public interface IAssemblyNameProvider
{
string GetAssemblyNameFromScriptPath(string path);
IEnumerable<Assembly> GetAllAssemblies(Func<string, bool> shouldFileBePartOfSolution);
IEnumerable<string> GetAllAssetPaths();
}
class AssemblyNameProvider : IAssemblyNameProvider
{
public string GetAssemblyNameFromScriptPath(string path)
{
return CompilationPipeline.GetAssemblyNameFromScriptPath(path);
}
public IEnumerable<Assembly> GetAllAssemblies(Func<string, bool> shouldFileBePartOfSolution)
{
return CompilationPipeline.GetAssemblies().Where(i => 0 < i.sourceFiles.Length && i.sourceFiles.Any(shouldFileBePartOfSolution));
}
public IEnumerable<string> GetAllAssetPaths()
{
return AssetDatabase.GetAllAssetPaths();
}
}
public class ProjectGeneration : IGenerator
{
enum ScriptingLanguage
{
None,
CSharp
}
public static readonly string MSBuildNamespaceUri = "http://schemas.microsoft.com/developer/msbuild/2003";
const string k_WindowsNewline = "\r\n";
/// <summary>
/// Map source extensions to ScriptingLanguages
/// </summary>
static readonly Dictionary<string, ScriptingLanguage> k_BuiltinSupportedExtensions = new Dictionary<string, ScriptingLanguage>
{
{ "cs", ScriptingLanguage.CSharp },
{ "uxml", ScriptingLanguage.None },
{ "uss", ScriptingLanguage.None },
{ "shader", ScriptingLanguage.None },
{ "compute", ScriptingLanguage.None },
{ "cginc", ScriptingLanguage.None },
{ "hlsl", ScriptingLanguage.None },
{ "glslinc", ScriptingLanguage.None }
};
string m_SolutionProjectEntryTemplate = string.Join("\r\n",
@"Project(""{{{0}}}"") = ""{1}"", ""{2}"", ""{{{3}}}""",
@"EndProject").Replace(" ", "\t");
string m_SolutionProjectConfigurationTemplate = string.Join("\r\n",
@" {{{0}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU",
@" {{{0}}}.Debug|Any CPU.Build.0 = Debug|Any CPU",
@" {{{0}}}.Release|Any CPU.ActiveCfg = Release|Any CPU",
@" {{{0}}}.Release|Any CPU.Build.0 = Release|Any CPU").Replace(" ", "\t");
static readonly string[] k_ReimportSyncExtensions = { ".dll", ".asmdef" };
static readonly Regex k_ScriptReferenceExpression = new Regex(
@"^Library.ScriptAssemblies.(?<dllname>(?<project>.*)\.dll$)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
string[] m_ProjectSupportedExtensions = new string[0];
public string ProjectDirectory { get; }
readonly string m_ProjectName;
readonly IAssemblyNameProvider m_AssemblyNameProvider;
public ProjectGeneration()
{
var projectDirectory = Directory.GetParent(Application.dataPath).FullName;
ProjectDirectory = projectDirectory.Replace('\\', '/');
m_ProjectName = Path.GetFileName(ProjectDirectory);
m_AssemblyNameProvider = new AssemblyNameProvider();
}
public ProjectGeneration(string tempDirectory) : this(tempDirectory, new AssemblyNameProvider()) {
}
public ProjectGeneration(string tempDirectory, IAssemblyNameProvider assemblyNameProvider) {
ProjectDirectory = tempDirectory.Replace('\\', '/');
m_ProjectName = Path.GetFileName(ProjectDirectory);
m_AssemblyNameProvider = assemblyNameProvider;
}
/// <summary>
/// Syncs the scripting solution if any affected files are relevant.
/// </summary>
/// <returns>
/// Whether the solution was synced.
/// </returns>
/// <param name='affectedFiles'>
/// A set of files whose status has changed
/// </param>
/// <param name="reimportedFiles">
/// A set of files that got reimported
/// </param>
public bool SyncIfNeeded(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles)
{
SetupProjectSupportedExtensions();
// Don't sync if we haven't synced before
if (HasSolutionBeenGenerated() && HasFilesBeenModified(affectedFiles, reimportedFiles))
{
Sync();
return true;
}
return false;
}
bool HasFilesBeenModified(IEnumerable<string> affectedFiles, IEnumerable<string> reimportedFiles)
{
return affectedFiles.Any(ShouldFileBePartOfSolution) || reimportedFiles.Any(ShouldSyncOnReimportedAsset);
}
static bool ShouldSyncOnReimportedAsset(string asset)
{
return k_ReimportSyncExtensions.Contains(new FileInfo(asset).Extension);
}
public void Sync()
{
SetupProjectSupportedExtensions();
bool externalCodeAlreadyGeneratedProjects = OnPreGeneratingCSProjectFiles();
if (!externalCodeAlreadyGeneratedProjects)
{
GenerateAndWriteSolutionAndProjects();
}
OnGeneratedCSProjectFiles();
}
public bool HasSolutionBeenGenerated()
{
return File.Exists(SolutionFile());
}
void SetupProjectSupportedExtensions()
{
m_ProjectSupportedExtensions = EditorSettings.projectGenerationUserExtensions;
}
bool ShouldFileBePartOfSolution(string file)
{
string extension = Path.GetExtension(file);
// Exclude files coming from packages except if they are internalized.
if (IsInternalizedPackagePath(file))
{
return false;
}
// Dll's are not scripts but still need to be included..
if (extension == ".dll")
return true;
if (file.ToLower().EndsWith(".asmdef"))
return true;
return IsSupportedExtension(extension);
}
bool IsSupportedExtension(string extension)
{
extension = extension.TrimStart('.');
if (k_BuiltinSupportedExtensions.ContainsKey(extension))
return true;
if (m_ProjectSupportedExtensions.Contains(extension))
return true;
return false;
}
static ScriptingLanguage ScriptingLanguageFor(Assembly island)
{
return ScriptingLanguageFor(GetExtensionOfSourceFiles(island.sourceFiles));
}
static string GetExtensionOfSourceFiles(string[] files)
{
return files.Length > 0 ? GetExtensionOfSourceFile(files[0]) : "NA";
}
static string GetExtensionOfSourceFile(string file)
{
var ext = Path.GetExtension(file).ToLower();
ext = ext.Substring(1); //strip dot
return ext;
}
static ScriptingLanguage ScriptingLanguageFor(string extension)
{
return k_BuiltinSupportedExtensions.TryGetValue(extension.TrimStart('.'), out var result)
? result
: ScriptingLanguage.None;
}
public void GenerateAndWriteSolutionAndProjects()
{
// Only synchronize islands that have associated source files and ones that we actually want in the project.
// This also filters out DLLs coming from .asmdef files in packages.
var assemblies = m_AssemblyNameProvider.GetAllAssemblies(ShouldFileBePartOfSolution);
var allAssetProjectParts = GenerateAllAssetProjectParts();
var monoIslands = assemblies.ToList();
SyncSolution(monoIslands);
var allProjectIslands = RelevantIslandsForMode(monoIslands).ToList();
foreach (Assembly assembly in allProjectIslands)
{
var responseFileData = ParseResponseFileData(assembly);
SyncProject(assembly, allAssetProjectParts, responseFileData, allProjectIslands);
}
}
IEnumerable<ResponseFileData> ParseResponseFileData(Assembly assembly)
{
var systemReferenceDirectories = CompilationPipeline.GetSystemAssemblyDirectories(assembly.compilerOptions.ApiCompatibilityLevel);
Dictionary<string, ResponseFileData> responseFilesData = assembly.compilerOptions.ResponseFiles.ToDictionary(x => x, x => CompilationPipeline.ParseResponseFile(
Path.Combine(ProjectDirectory, x),
ProjectDirectory,
systemReferenceDirectories
));
Dictionary<string, ResponseFileData> responseFilesWithErrors = responseFilesData.Where(x => x.Value.Errors.Any())
.ToDictionary(x => x.Key, x => x.Value);
if (responseFilesWithErrors.Any())
{
foreach (var error in responseFilesWithErrors)
foreach (var valueError in error.Value.Errors)
{
Debug.LogError($"{error.Key} Parse Error : {valueError}");
}
}
return responseFilesData.Select(x => x.Value);
}
Dictionary<string, string> GenerateAllAssetProjectParts()
{
Dictionary<string, StringBuilder> stringBuilders = new Dictionary<string, StringBuilder>();
foreach (string asset in m_AssemblyNameProvider.GetAllAssetPaths())
{
// Exclude files coming from packages except if they are internalized.
if (IsInternalizedPackagePath(asset))
{
continue;
}
string extension = Path.GetExtension(asset);
if (IsSupportedExtension(extension) && ScriptingLanguage.None == ScriptingLanguageFor(extension))
{
// Find assembly the asset belongs to by adding script extension and using compilation pipeline.
var assemblyName = m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset + ".cs");
assemblyName = assemblyName ?? m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset + ".js");
assemblyName = assemblyName ?? m_AssemblyNameProvider.GetAssemblyNameFromScriptPath(asset + ".boo");
if (string.IsNullOrEmpty(assemblyName))
{
continue;
}
assemblyName = Utility.FileNameWithoutExtension(assemblyName);
if (!stringBuilders.TryGetValue(assemblyName, out var projectBuilder))
{
projectBuilder = new StringBuilder();
stringBuilders[assemblyName] = projectBuilder;
}
projectBuilder.Append(" <None Include=\"").Append(EscapedRelativePathFor(asset)).Append("\" />").Append(k_WindowsNewline);
}
}
var result = new Dictionary<string, string>();
foreach (var entry in stringBuilders)
result[entry.Key] = entry.Value.ToString();
return result;
}
static bool IsInternalizedPackagePath(string file)
{
if (string.IsNullOrWhiteSpace(file))
{
return false;
}
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(file);
if (packageInfo == null) {
return false;
}
var packageSource = packageInfo.source;
return packageSource != PackageSource.Embedded && packageSource != PackageSource.Local;
}
void SyncProject(
Assembly island,
Dictionary<string, string> allAssetsProjectParts,
IEnumerable<ResponseFileData> responseFilesData,
List<Assembly> allProjectIslands)
{
SyncProjectFileIfNotChanged(ProjectFile(island), ProjectText(island, allAssetsProjectParts, responseFilesData, allProjectIslands));
}
static void SyncProjectFileIfNotChanged(string path, string newContents)
{
if (Path.GetExtension(path) == ".csproj")
{
newContents = OnGeneratedCSProject(path, newContents);
}
SyncFileIfNotChanged(path, newContents);
}
static void SyncSolutionFileIfNotChanged(string path, string newContents)
{
newContents = OnGeneratedSlnSolution(path, newContents);
SyncFileIfNotChanged(path, newContents);
}
static void OnGeneratedCSProjectFiles()
{
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
.Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);
var args = new object[0];
foreach (var type in types)
{
var method = type.GetMethod("OnGeneratedCSProjectFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
method.Invoke(null, args);
}
}
static bool OnPreGeneratingCSProjectFiles()
{
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
.Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);
bool result = false;
foreach (var type in types)
{
var args = new object[0];
var method = type.GetMethod("OnPreGeneratingCSProjectFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(bool))
{
result |= (bool)returnValue;
}
}
return result;
}
static string OnGeneratedCSProject(string path, string content)
{
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
.Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);
foreach (var type in types)
{
var args = new [] { path, content };
var method = type.GetMethod("OnGeneratedCSProject", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(string))
{
content = (string)returnValue;
}
}
return content;
}
static string OnGeneratedSlnSolution(string path, string content)
{
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
.Where(x => typeof(AssetPostprocessor).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);
foreach (var type in types)
{
var args = new [] { path, content };
var method = type.GetMethod("OnGeneratedSlnSolution", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (method == null)
{
continue;
}
var returnValue = method.Invoke(null, args);
if (method.ReturnType == typeof(string))
{
content = (string)returnValue;
}
}
return content;
}
static void SyncFileIfNotChanged(string filename, string newContents)
{
if (File.Exists(filename) &&
newContents == File.ReadAllText(filename))
{
return;
}
File.WriteAllText(filename, newContents, Encoding.UTF8);
}
string ProjectText(Assembly assembly,
Dictionary<string, string> allAssetsProjectParts,
IEnumerable<ResponseFileData> responseFilesData,
List<Assembly> allProjectIslands)
{
var projectBuilder = new StringBuilder(ProjectHeader(assembly, responseFilesData));
var references = new List<string>();
var projectReferences = new List<Match>();
foreach (string file in assembly.sourceFiles)
{
if (!ShouldFileBePartOfSolution(file))
continue;
var extension = Path.GetExtension(file).ToLower();
var fullFile = EscapedRelativePathFor(file);
if (".dll" != extension)
{
projectBuilder.Append(" <Compile Include=\"").Append(fullFile).Append("\" />").Append(k_WindowsNewline);
}
else
{
references.Add(fullFile);
}
}
var assemblyName = Utility.FileNameWithoutExtension(assembly.outputPath);
// Append additional non-script files that should be included in project generation.
if (allAssetsProjectParts.TryGetValue(assemblyName, out var additionalAssetsForProject))
projectBuilder.Append(additionalAssetsForProject);
var islandRefs = references.Union(assembly.allReferences);
foreach (string reference in islandRefs)
{
if (reference.EndsWith("/UnityEditor.dll", StringComparison.Ordinal)
|| reference.EndsWith("/UnityEngine.dll", StringComparison.Ordinal)
|| reference.EndsWith("\\UnityEditor.dll", StringComparison.Ordinal)
|| reference.EndsWith("\\UnityEngine.dll", StringComparison.Ordinal))
continue;
var match = k_ScriptReferenceExpression.Match(reference);
if (match.Success)
{
// assume csharp language
// Add a reference to a project except if it's a reference to a script assembly
// that we are not generating a project for. This will be the case for assemblies
// coming from .assembly.json files in non-internalized packages.
var dllName = match.Groups["dllname"].Value;
if (allProjectIslands.Any(i => Path.GetFileName(i.outputPath) == dllName))
{
projectReferences.Add(match);
continue;
}
}
string fullReference = Path.IsPathRooted(reference) ? reference : Path.Combine(ProjectDirectory, reference);
AppendReference(fullReference, projectBuilder);
}
var responseRefs = responseFilesData.SelectMany(x => x.FullPathReferences.Select(r => r));
foreach (var reference in responseRefs)
{
AppendReference(reference, projectBuilder);
}
if (0 < projectReferences.Count)
{
projectBuilder.AppendLine(" </ItemGroup>");
projectBuilder.AppendLine(" <ItemGroup>");
foreach (Match reference in projectReferences)
{
var referencedProject = reference.Groups["project"].Value;
projectBuilder.Append(" <ProjectReference Include=\"").Append(referencedProject).Append(GetProjectExtension()).Append("\">").Append(k_WindowsNewline);
projectBuilder.Append(" <Project>{").Append(ProjectGuid(Path.Combine("Temp", reference.Groups["project"].Value + ".dll"))).Append("}</Project>").Append(k_WindowsNewline);
projectBuilder.Append(" <Name>").Append(referencedProject).Append("</Name>").Append(k_WindowsNewline);
projectBuilder.AppendLine(" </ProjectReference>");
}
}
projectBuilder.Append(ProjectFooter());
return projectBuilder.ToString();
}
static void AppendReference(string fullReference, StringBuilder projectBuilder)
{
//replace \ with / and \\ with /
var escapedFullPath = SecurityElement.Escape(fullReference);
escapedFullPath = escapedFullPath.Replace("\\", "/");
escapedFullPath = escapedFullPath.Replace("\\\\", "/");
projectBuilder.Append(" <Reference Include=\"").Append(Utility.FileNameWithoutExtension(escapedFullPath)).Append("\">").Append(k_WindowsNewline);
projectBuilder.Append(" <HintPath>").Append(escapedFullPath).Append("</HintPath>").Append(k_WindowsNewline);
projectBuilder.Append(" </Reference>").Append(k_WindowsNewline);
}
public string ProjectFile(Assembly assembly)
{
return Path.Combine(ProjectDirectory, $"{Utility.FileNameWithoutExtension(assembly.outputPath)}.csproj");
}
public string SolutionFile()
{
return Path.Combine(ProjectDirectory, $"{m_ProjectName}.sln");
}
string ProjectHeader(
Assembly island,
IEnumerable<ResponseFileData> responseFilesData
)
{
var toolsVersion = "4.0";
var productVersion = "10.0.20506";
const string baseDirectory = ".";
var targetFrameworkVersion = "v4.7.1";
var targetLanguageVersion = "latest";
var arguments = new object[]
{
toolsVersion, productVersion, ProjectGuid(island.outputPath),
InternalEditorUtility.GetEngineAssemblyPath(),
InternalEditorUtility.GetEditorAssemblyPath(),
string.Join(";", new[] { "DEBUG", "TRACE" }.Concat(EditorUserBuildSettings.activeScriptCompilationDefines).Concat(island.defines).Concat(responseFilesData.SelectMany(x => x.Defines)).Distinct().ToArray()),
MSBuildNamespaceUri,
Utility.FileNameWithoutExtension(island.outputPath),
EditorSettings.projectGenerationRootNamespace,
targetFrameworkVersion,
targetLanguageVersion,
baseDirectory,
island.compilerOptions.AllowUnsafeCode | responseFilesData.Any(x => x.Unsafe)
};
try
{
return string.Format(GetProjectHeaderTemplate(), arguments);
}
catch (Exception)
{
throw new NotSupportedException("Failed creating c# project because the c# project header did not have the correct amount of arguments, which is " + arguments.Length);
}
}
static string GetSolutionText()
{
return string.Join("\r\n",
@"",
@"Microsoft Visual Studio Solution File, Format Version {0}",
@"# Visual Studio {1}",
@"{2}",
@"Global",
@" GlobalSection(SolutionConfigurationPlatforms) = preSolution",
@" Debug|Any CPU = Debug|Any CPU",
@" Release|Any CPU = Release|Any CPU",
@" EndGlobalSection",
@" GlobalSection(ProjectConfigurationPlatforms) = postSolution",
@"{3}",
@" EndGlobalSection",
@" GlobalSection(SolutionProperties) = preSolution",
@" HideSolutionNode = FALSE",
@" EndGlobalSection",
@"EndGlobal",
@"").Replace(" ", "\t");
}
static string GetProjectFooterTemplate()
{
return string.Join("\r\n",
@" </ItemGroup>",
@" <Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />",
@" <!-- 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>",
@"");
}
static string GetProjectHeaderTemplate()
{
var header = new[]
{
@"<?xml version=""1.0"" encoding=""utf-8""?>",
@"<Project ToolsVersion=""{0}"" DefaultTargets=""Build"" xmlns=""{6}"">",
@" <PropertyGroup>",
@" <LangVersion>{10}</LangVersion>",
@" </PropertyGroup>",
@" <PropertyGroup>",
@" <Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>",
@" <Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>",
@" <ProductVersion>{1}</ProductVersion>",
@" <SchemaVersion>2.0</SchemaVersion>",
@" <RootNamespace>{8}</RootNamespace>",
@" <ProjectGuid>{{{2}}}</ProjectGuid>",
@" <OutputType>Library</OutputType>",
@" <AppDesignerFolder>Properties</AppDesignerFolder>",
@" <AssemblyName>{7}</AssemblyName>",
@" <TargetFrameworkVersion>{9}</TargetFrameworkVersion>",
@" <FileAlignment>512</FileAlignment>",
@" <BaseDirectory>{11}</BaseDirectory>",
@" </PropertyGroup>",
@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "">",
@" <DebugSymbols>true</DebugSymbols>",
@" <DebugType>full</DebugType>",
@" <Optimize>false</Optimize>",
@" <OutputPath>Temp\bin\Debug\</OutputPath>",
@" <DefineConstants>{5}</DefineConstants>",
@" <ErrorReport>prompt</ErrorReport>",
@" <WarningLevel>4</WarningLevel>",
@" <NoWarn>0169</NoWarn>",
@" <AllowUnsafeBlocks>{12}</AllowUnsafeBlocks>",
@" </PropertyGroup>",
@" <PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "">",
@" <DebugType>pdbonly</DebugType>",
@" <Optimize>true</Optimize>",
@" <OutputPath>Temp\bin\Release\</OutputPath>",
@" <ErrorReport>prompt</ErrorReport>",
@" <WarningLevel>4</WarningLevel>",
@" <NoWarn>0169</NoWarn>",
@" <AllowUnsafeBlocks>{12}</AllowUnsafeBlocks>",
@" </PropertyGroup>"
};
var forceExplicitReferences = new[]
{
@" <PropertyGroup>",
@" <NoConfig>true</NoConfig>",
@" <NoStdLib>true</NoStdLib>",
@" <AddAdditionalExplicitAssemblyReferences>false</AddAdditionalExplicitAssemblyReferences>",
@" <ImplicitlyExpandNETStandardFacades>false</ImplicitlyExpandNETStandardFacades>",
@" <ImplicitlyExpandDesignTimeFacades>false</ImplicitlyExpandDesignTimeFacades>",
@" </PropertyGroup>"
};
var itemGroupStart = new[]
{
@" <ItemGroup>"
};
var footer = new[]
{
@" <Reference Include=""UnityEngine"">",
@" <HintPath>{3}</HintPath>",
@" </Reference>",
@" <Reference Include=""UnityEditor"">",
@" <HintPath>{4}</HintPath>",
@" </Reference>",
@" </ItemGroup>",
@" <ItemGroup>",
@""
};
var text = header.Concat(forceExplicitReferences).Concat(itemGroupStart).Concat(footer).ToArray();
return string.Join("\r\n", text);
}
void SyncSolution(IEnumerable<Assembly> islands)
{
SyncSolutionFileIfNotChanged(SolutionFile(), SolutionText(islands));
}
string SolutionText(IEnumerable<Assembly> islands)
{
var fileversion = "11.00";
var vsversion = "2010";
var relevantIslands = RelevantIslandsForMode(islands);
string projectEntries = GetProjectEntries(relevantIslands);
string projectConfigurations = string.Join(k_WindowsNewline, relevantIslands.Select(i => GetProjectActiveConfigurations(ProjectGuid(i.outputPath))).ToArray());
return string.Format(GetSolutionText(), fileversion, vsversion, projectEntries, projectConfigurations);
}
static IEnumerable<Assembly> RelevantIslandsForMode(IEnumerable<Assembly> islands)
{
IEnumerable<Assembly> relevantIslands = islands.Where(i => ScriptingLanguage.CSharp == ScriptingLanguageFor(i));
return relevantIslands;
}
/// <summary>
/// Get a Project("{guid}") = "MyProject", "MyProject.unityproj", "{projectguid}"
/// entry for each relevant language
/// </summary>
string GetProjectEntries(IEnumerable<Assembly> islands)
{
var projectEntries = islands.Select(i => string.Format(
m_SolutionProjectEntryTemplate,
SolutionGuid(i), Utility.FileNameWithoutExtension(i.outputPath), Path.GetFileName(ProjectFile(i)), ProjectGuid(i.outputPath)
));
return string.Join(k_WindowsNewline, projectEntries.ToArray());
}
/// <summary>
/// Generate the active configuration string for a given project guid
/// </summary>
string GetProjectActiveConfigurations(string projectGuid)
{
return string.Format(
m_SolutionProjectConfigurationTemplate,
projectGuid);
}
string EscapedRelativePathFor(string file)
{
var projectDir = ProjectDirectory.Replace('/', '\\');
file = file.Replace('/', '\\');
var path = SkipPathPrefix(file, projectDir);
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(path.Replace('\\', '/'));
if (packageInfo != null) {
// We have to normalize the path, because the PackageManagerRemapper assumes
// dir seperators will be os specific.
var absolutePath = Path.GetFullPath(NormalizePath(path)).Replace('/', '\\');
path = SkipPathPrefix(absolutePath, projectDir);
}
return SecurityElement.Escape(path);
}
static string SkipPathPrefix(string path, string prefix)
{
if (path.StartsWith(prefix))
return path.Substring(prefix.Length + 1);
return path;
}
static string NormalizePath(string path)
{
if (Path.DirectorySeparatorChar == '\\')
return path.Replace('/', Path.DirectorySeparatorChar);
return path.Replace('\\', Path.DirectorySeparatorChar);
}
string ProjectGuid(string assembly)
{
return SolutionGuidGenerator.GuidForProject(m_ProjectName + Utility.FileNameWithoutExtension(assembly));
}
string SolutionGuid(Assembly island)
{
return SolutionGuidGenerator.GuidForSolution(m_ProjectName, GetExtensionOfSourceFiles(island.sourceFiles));
}
static string ProjectFooter()
{
return GetProjectFooterTemplate();
}
static string GetProjectExtension()
{
return ".csproj";
}
}
public static class SolutionGuidGenerator
{
public static string GuidForProject(string projectName)
{
return ComputeGuidHashFor(projectName + "salt");
}
public static string GuidForSolution(string projectName, string sourceFileExtension)
{
if (sourceFileExtension.ToLower() == "cs")
// GUID for a C# class library: http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs
return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC";
return ComputeGuidHashFor(projectName);
}
static string ComputeGuidHashFor(string input)
{
var hash = MD5.Create().ComputeHash(Encoding.Default.GetBytes(input));
return HashAsGuid(HashToString(hash));
}
static string HashAsGuid(string hash)
{
var guid = hash.Substring(0, 8) + "-" + hash.Substring(8, 4) + "-" + hash.Substring(12, 4) + "-" + hash.Substring(16, 4) + "-" + hash.Substring(20, 12);
return guid.ToUpper();
}
static string HashToString(byte[] bs)
{
var sb = new StringBuilder();
foreach (byte b in bs)
sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
}

View File

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

35
Editor/Utility.cs Normal file
View File

@@ -0,0 +1,35 @@
namespace VisualStudioEditor
{
public static class Utility
{
public static string FileNameWithoutExtension(string path)
{
if (string.IsNullOrEmpty(path))
{
return "";
}
var indexOfDot = -1;
var indexOfSlash = 0;
for (var i = path.Length - 1; i >= 0; i--)
{
if (indexOfDot == -1 && path[i] == '.')
{
indexOfDot = i;
}
if (indexOfSlash == 0 && path[i] == '/' || path[i] == '\\')
{
indexOfSlash = i + 1;
break;
}
}
if (indexOfDot == -1)
{
indexOfDot = path.Length - 1;
}
return path.Substring(indexOfSlash, indexOfDot - indexOfSlash);
}
}
}

11
Editor/Utility.cs.meta Normal file
View File

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

448
Editor/VSEditor.cs Normal file
View File

@@ -0,0 +1,448 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Win32;
using UnityEditor;
using UnityEngine;
using Debug = System.Diagnostics.Debug;
using Unity.CodeEditor;
namespace VisualStudioEditor
{
public enum VisualStudioVersion
{
Invalid = 0,
VisualStudio2008 = 9,
VisualStudio2010 = 10,
VisualStudio2012 = 11,
VisualStudio2013 = 12,
VisualStudio2015 = 14,
VisualStudio2017 = 15,
VisualStudio2019 = 16,
}
[InitializeOnLoad]
public class VSEditor : IExternalCodeEditor
{
static readonly string k_ExpressNotSupportedMessage = L10n.Tr(
"Unfortunately Visual Studio Express does not allow itself to be controlled by external applications. " +
"You can still use it by manually opening the Visual Studio project file, but Unity cannot automatically open files for you when you doubleclick them. " +
"\n(This does work with Visual Studio Pro)"
);
static IEnumerable<string> FindVisualStudioDevEnvPaths()
{
string asset = AssetDatabase.FindAssets("VSWhere a:packages").Select(AssetDatabase.GUIDToAssetPath).FirstOrDefault(assetPath => assetPath.Contains("vswhere.exe"));
if (string.IsNullOrWhiteSpace(asset)) // This may be called too early where the asset database has not replicated this information yet.
{
yield break;
}
UnityEditor.PackageManager.PackageInfo packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(asset);
var progpath = packageInfo.resolvedPath + asset.Substring("Packages/com.unity.ide.visualstudio".Length);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = progpath,
Arguments = "-prerelease -property productPath",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
}
};
process.Start();
process.WaitForExit();
while (!process.StandardOutput.EndOfStream)
{
yield return process.StandardOutput.ReadLine();
}
}
static VSEditor()
{
try
{
InstalledVisualStudios = GetInstalledVisualStudios();
}
catch (Exception ex)
{
UnityEngine.Debug.Log($@"Error detecting Visual Studio installations: {ex.Message}{Environment.NewLine}{ex.StackTrace}");
InstalledVisualStudios = new Dictionary<VisualStudioVersion, string[]>();
}
var editor = new VSEditor(new Discovery(), new ProjectGeneration());
CodeEditor.Register(editor);
var current = CodeEditor.CurrentEditorInstallation;
if (editor.TryGetInstallationForPath(current, out var installation))
{
editor.Initialize(current);
return;
}
}
IDiscovery m_Discoverability;
IGenerator m_Generation;
CodeEditor.Installation m_Installation;
VSInitializer m_Initiliazer = new VSInitializer();
bool m_ExternalEditorSupportsUnityProj;
public VSEditor(IDiscovery discovery, IGenerator projectGeneration)
{
m_Discoverability = discovery;
m_Generation = projectGeneration;
}
internal static Dictionary<VisualStudioVersion, string[]> InstalledVisualStudios { get; private set; }
static bool IsOSX => Environment.OSVersion.Platform == PlatformID.Unix;
static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == '\\' && Environment.NewLine == "\r\n";
static readonly GUIContent k_AddUnityProjeToSln = EditorGUIUtility.TrTextContent("Add .unityproj's to .sln");
static string GetRegistryValue(string path, string key)
{
try
{
return Registry.GetValue(path, key, null) as string;
}
catch (Exception)
{
return "";
}
}
/// <summary>
/// Derives the Visual Studio installation path from the debugger path
/// </summary>
/// <returns>
/// The Visual Studio installation path (to devenv.exe)
/// </returns>
/// <param name='debuggerPath'>
/// The debugger path from the windows registry
/// </param>
static string DeriveVisualStudioPath(string debuggerPath)
{
string startSentinel = DeriveProgramFilesSentinel();
string endSentinel = "Common7";
bool started = false;
string[] tokens = debuggerPath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
string path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
// Walk directories in debugger path, chop out "Program Files\INSTALLATION\PATH\HERE\Common7"
foreach (var token in tokens)
{
if (!started && string.Equals(startSentinel, token, StringComparison.OrdinalIgnoreCase))
{
started = true;
continue;
}
if (started)
{
path = Path.Combine(path, token);
if (string.Equals(endSentinel, token, StringComparison.OrdinalIgnoreCase))
break;
}
}
return Path.Combine(path, "IDE", "devenv.exe");
}
/// <summary>
/// Derives the program files sentinel for grabbing the VS installation path.
/// </summary>
/// <remarks>
/// From a path like 'c:\Archivos de programa (x86)', returns 'Archivos de programa'
/// </remarks>
static string DeriveProgramFilesSentinel()
{
string path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.LastOrDefault();
if (!string.IsNullOrEmpty(path))
{
// This needs to be the "real" Program Files regardless of 64bitness
int index = path.LastIndexOf("(x86)");
if (0 <= index)
path = path.Remove(index);
return path.TrimEnd();
}
return "Program Files";
}
public CodeEditor.Installation[] Installations => m_Discoverability.PathCallback();
public static void ParseRawDevEnvPaths(string[] rawDevEnvPaths, Dictionary<VisualStudioVersion, string[]> versions)
{
if (rawDevEnvPaths == null)
{
return;
}
var v2017 = rawDevEnvPaths.Where(path => path.Contains("2017")).ToArray();
var v2019 = rawDevEnvPaths.Where(path => path.Contains("2019")).ToArray();
if (v2017.Length > 0)
{
versions[VisualStudioVersion.VisualStudio2017] = v2017;
}
if (v2019.Length > 0)
{
versions[VisualStudioVersion.VisualStudio2019] = v2019;
}
}
/// <summary>
/// Detects Visual Studio installations using the Windows registry
/// </summary>
/// <returns>
/// The detected Visual Studio installations
/// </returns>
internal static Dictionary<VisualStudioVersion, string[]> GetInstalledVisualStudios()
{
var versions = new Dictionary<VisualStudioVersion, string[]>();
if (IsWindows)
{
foreach (VisualStudioVersion version in Enum.GetValues(typeof(VisualStudioVersion)))
{
if (version > VisualStudioVersion.VisualStudio2015)
continue;
try
{
// Try COMNTOOLS environment variable first
FindLegacyVisualStudio(version, versions);
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
}
}
var raw = FindVisualStudioDevEnvPaths();
ParseRawDevEnvPaths(raw.ToArray(), versions);
}
return versions;
}
static void FindLegacyVisualStudio(VisualStudioVersion version, Dictionary<VisualStudioVersion, string[]> versions)
{
string key = Environment.GetEnvironmentVariable($"VS{(int)version}0COMNTOOLS");
if (!string.IsNullOrEmpty(key))
{
string path = Path.Combine(key, "..", "IDE", "devenv.exe");
if (File.Exists(path))
{
versions[version] = new[] { path };
return;
}
}
// Try the proper registry key
key = GetRegistryValue(
$@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\{(int)version}.0", "InstallDir");
// Try to fallback to the 32bits hive
if (string.IsNullOrEmpty(key))
key = GetRegistryValue(
$@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\{(int)version}.0", "InstallDir");
if (!string.IsNullOrEmpty(key))
{
string path = Path.Combine(key, "devenv.exe");
if (File.Exists(path))
{
versions[version] = new[] { path };
return;
}
}
// Fallback to debugger key
key = GetRegistryValue(
// VS uses this key for the local debugger path
$@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\{(int)version}.0\Debugger", "FEQARuntimeImplDll");
if (!string.IsNullOrEmpty(key))
{
string path = DeriveVisualStudioPath(key);
if (!string.IsNullOrEmpty(path) && File.Exists(path))
versions[version] = new[] { DeriveVisualStudioPath(key) };
}
}
public void CreateIfDoesntExist()
{
if (!m_Generation.HasSolutionBeenGenerated())
{
m_Generation.Sync();
}
}
public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
{
var lowerCasePath = editorPath.ToLower();
if (lowerCasePath.EndsWith("vcsexpress.exe"))
{
installation = new CodeEditor.Installation
{
Name = "VSExpress",
Path = editorPath
};
m_Installation = installation;
return true;
}
if (lowerCasePath.EndsWith("devenv.exe"))
{
installation = new CodeEditor.Installation
{
Name = "VisualStudio",
Path = editorPath
};
m_Installation = installation;
return true;
}
var filename = Path.GetFileName(lowerCasePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)).Replace(" ", "");
if (filename == "visualstudio.app" || filename == "visualstudio(preview).app" || lowerCasePath.Contains("monodevelop") || lowerCasePath.Contains("xamarinstudio") || lowerCasePath.Contains("xamarin studio"))
{
installation = new CodeEditor.Installation
{
Name = "MonoDevelop",
Path = editorPath
};
m_Installation = installation;
return true;
}
installation = default;
m_Installation = installation;
return false;
}
public void OnGUI()
{
if (m_Installation.Name.Equals("VSExpress"))
{
GUILayout.BeginHorizontal(EditorStyles.helpBox);
GUILayout.Label("", "CN EntryWarn");
GUILayout.Label(k_ExpressNotSupportedMessage, "WordWrappedLabel");
GUILayout.EndHorizontal();
}
if (m_Installation.Name.Equals("MonoDevelop"))
{
m_ExternalEditorSupportsUnityProj = EditorGUILayout.Toggle(
k_AddUnityProjeToSln,
m_ExternalEditorSupportsUnityProj);
}
}
public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles)
{
m_Generation.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
}
public void SyncAll()
{
m_Generation.Sync();
}
public void Initialize(string editorInstallationPath)
{
CreateIfDoesntExist();
m_Initiliazer.Initialize(editorInstallationPath, InstalledVisualStudios);
}
public bool OpenProject(string path, int line, int column)
{
var comAssetPath = AssetDatabase.FindAssets("COMIntegration a:packages").Select(AssetDatabase.GUIDToAssetPath).First(assetPath => assetPath.Contains("COMIntegration.exe"));
if (string.IsNullOrWhiteSpace(comAssetPath)) // This may be called too early where the asset database has not replicated this information yet.
{
return false;
}
UnityEditor.PackageManager.PackageInfo packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(comAssetPath);
var progpath = packageInfo.resolvedPath + comAssetPath.Substring("Packages/com.unity.ide.visualstudio".Length);
var fileInfo = new FileInfo(path).FullName;
var solution = GetSolutionFile(path);
if (solution == "")
{
m_Generation.Sync();
solution = GetSolutionFile(path);
}
solution = solution == "" ? "" : $"\"{solution}\"";
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = progpath,
Arguments = $"\"{CodeEditor.CurrentEditorInstallation}\" \"{fileInfo}\" {solution} {line}",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
}
};
var result = process.Start();
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);
}
if (outputLine == "clearprogressbar")
{
EditorUtility.ClearProgressBar();
}
}
var errorOutput = process.StandardError.ReadToEnd();
if (!string.IsNullOrEmpty(errorOutput))
{
UnityEngine.Debug.Log("Error: \n" + errorOutput);
}
process.WaitForExit();
return result;
}
private string GetSolutionFile(string path)
{
if (UnityEditor.Unsupported.IsDeveloperBuild())
{
var baseFolder = GetBaseUnityDeveloperFolder();
var lowerPath = path.ToLowerInvariant();
var isUnitySourceCode =
lowerPath.Contains((baseFolder + "/Runtime").ToLowerInvariant())
|| lowerPath.Contains((baseFolder + "/Editor").ToLowerInvariant());
if (isUnitySourceCode)
{
return Path.Combine(baseFolder, "Projects/CSharp/Unity.CSharpProjects.gen.sln");
}
}
var solutionFile = m_Generation.SolutionFile();
if (File.Exists(solutionFile))
{
return solutionFile;
}
return "";
}
private static string GetBaseUnityDeveloperFolder()
{
return Directory.GetParent(EditorApplication.applicationPath).Parent.Parent.FullName;
}
}
}

11
Editor/VSEditor.cs.meta Normal file
View File

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

270
Editor/VSInitializer.cs Normal file
View File

@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Win32;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace VisualStudioEditor
{
internal class VSInitializer
{
public void Initialize(string editorPath, Dictionary<VisualStudioVersion, string[]> installedVisualStudios)
{
switch (Application.platform) {
case RuntimePlatform.OSXEditor:
InitializeVSForMac(editorPath);
break;
case RuntimePlatform.WindowsEditor:
InitializeVisualStudio(editorPath, installedVisualStudios);
break;
}
}
static void InitializeVSForMac(string externalEditor)
{
if (!IsVSForMac(externalEditor, out var vsfmVersion))
return;
var bridgeFile = GetVSForMacBridgeAssembly(externalEditor, vsfmVersion);
if (string.IsNullOrEmpty(bridgeFile) || !File.Exists(bridgeFile))
{
Debug.Log("Unable to find Tools for Unity bridge dll for Visual Studio for Mac " + externalEditor);
return;
}
AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(bridgeFile));
}
static bool IsVisualStudioForMac(string path)
{
var lowerCasePath = path.ToLower();
var filename = Path.GetFileName(lowerCasePath).Replace(" ", "");
return filename.StartsWith("visualstudio") && !filename.Contains("code") && filename.EndsWith(".app");
}
static bool IsVSForMac(string externalEditor, out Version vsfmVersion)
{
vsfmVersion = null;
if (!IsVisualStudioForMac(externalEditor))
return false;
// We need to extract the version used by VS for Mac
// to lookup its addin registry
try
{
return GetVSForMacVersion(externalEditor, out vsfmVersion);
}
catch (Exception e)
{
Debug.Log("Failed to read Visual Studio for Mac information");
Debug.LogException(e);
return false;
}
}
static bool GetVSForMacVersion(string externalEditor, out Version vsfmVersion)
{
vsfmVersion = null;
// Read the full VS for Mac version from the plist, it will look like this:
//
// <key>CFBundleShortVersionString</key>
// <string>X.X.X.X</string>
var plist = Path.Combine(externalEditor, "Contents/Info.plist");
if (!File.Exists(plist))
return false;
const string versionStringRegex = @"\<key\>CFBundleShortVersionString\</key\>\s+\<string\>(?<version>\d+\.\d+\.\d+\.\d+?)\</string\>";
var file = File.ReadAllText(plist);
var match = Regex.Match(file, versionStringRegex);
var versionGroup = match.Groups["version"];
if (!versionGroup.Success)
return false;
vsfmVersion = new Version(versionGroup.Value);
return true;
}
static void InitializeVisualStudio(string externalEditor, Dictionary<VisualStudioVersion, string[]> installedVisualStudios)
{
if (!externalEditor.Contains("2017"))
{
return;
}
FindVisualStudio(externalEditor, out var vsVersion, installedVisualStudios);
var bridgeFile = GetVstuBridgeAssembly(vsVersion);
if (bridgeFile == null)
{
Debug.Log("Unable to find bridge dll in registry for Microsoft Visual Studio Tools for Unity for " + externalEditor);
return;
}
if (!File.Exists(bridgeFile))
{
Debug.Log("Unable to find bridge dll on disk for Microsoft Visual Studio Tools for Unity for " + bridgeFile);
return;
}
AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(bridgeFile));
}
static string GetVstuBridgeAssembly(VisualStudioVersion version)
{
try
{
var vsVersion = string.Empty;
switch (version)
{
// Starting with VS 15, the registry key is using the VS version
// to avoid taking a dependency on the product name
case VisualStudioVersion.VisualStudio2017:
vsVersion = "15.0";
break;
// VS 2015 and under are still installed in the registry
// using their project names
case VisualStudioVersion.VisualStudio2015:
vsVersion = "2015";
break;
case VisualStudioVersion.VisualStudio2013:
vsVersion = "2013";
break;
case VisualStudioVersion.VisualStudio2012:
vsVersion = "2012";
break;
case VisualStudioVersion.VisualStudio2010:
vsVersion = "2010";
break;
}
// search first for the current user with a fallback to machine wide setting
return GetVstuBridgePathFromRegistry(vsVersion, true)
?? GetVstuBridgePathFromRegistry(vsVersion, false);
}
catch (Exception)
{
return null;
}
}
static string GetVstuBridgePathFromRegistry(string vsVersion, bool currentUser)
{
var registryKey = $@"{(currentUser ? "HKEY_CURRENT_USER" : "HKEY_LOCAL_MACHINE")}\Software\Microsoft\Microsoft Visual Studio {vsVersion} Tools for Unity";
return (string)Registry.GetValue(registryKey, "UnityExtensionPath", null);
}
static void FindVisualStudio(string externalEditor, out VisualStudioVersion vsVersion, Dictionary<VisualStudioVersion, string[]> installedVisualStudios)
{
if (string.IsNullOrEmpty(externalEditor))
{
vsVersion = VisualStudioVersion.Invalid;
return;
}
// If it's a VS found through envvars or the registry
var matches = installedVisualStudios.Where(kvp => kvp.Value.Any(v => Path.GetFullPath(v).Equals(Path.GetFullPath(externalEditor), StringComparison.OrdinalIgnoreCase))).ToArray();
if (matches.Length > 0)
{
vsVersion = matches[0].Key;
return;
}
// If it's a side-by-side VS selected manually
if (externalEditor.EndsWith("devenv.exe", StringComparison.OrdinalIgnoreCase))
{
if (TryGetVisualStudioVersion(externalEditor, out vsVersion)) return;
}
vsVersion = VisualStudioVersion.Invalid;
}
static bool TryGetVisualStudioVersion(string externalEditor, out VisualStudioVersion vsVersion)
{
switch (ProductVersion(externalEditor).Major)
{
case 9:
vsVersion = VisualStudioVersion.VisualStudio2008;
return true;
case 10:
vsVersion = VisualStudioVersion.VisualStudio2010;
return true;
case 11:
vsVersion = VisualStudioVersion.VisualStudio2012;
return true;
case 12:
vsVersion = VisualStudioVersion.VisualStudio2013;
return true;
case 14:
vsVersion = VisualStudioVersion.VisualStudio2015;
return true;
case 15:
vsVersion = VisualStudioVersion.VisualStudio2017;
return true;
}
vsVersion = VisualStudioVersion.Invalid;
return false;
}
static Version ProductVersion(string externalEditor)
{
try
{
return new Version(FileVersionInfo.GetVersionInfo(externalEditor).ProductVersion);
}
catch (Exception)
{
return new Version(0, 0);
}
}
static string GetVSForMacBridgeAssembly(string externalEditor, Version vsfmVersion)
{
// Check first if we're overriden
// Useful when developing UnityVS for Mac
var bridge = Environment.GetEnvironmentVariable("VSTUM_BRIDGE");
if (!string.IsNullOrEmpty(bridge) && File.Exists(bridge))
return bridge;
// Look for installed addin
const string addinBridge = "Editor/SyntaxTree.VisualStudio.Unity.Bridge.dll";
const string addinName = "MonoDevelop.Unity";
// Check if we're installed in the user addins repository
// ~/Library/Application Support/VisualStudio/X.0/LocalInstall/Addins
var localAddins = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
"Library/Application Support/VisualStudio/" + vsfmVersion.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 a bridge, which is the one MD will load
if (Directory.Exists(localAddins))
{
foreach (var folder in Directory.GetDirectories(localAddins, addinName + "*", SearchOption.TopDirectoryOnly))
{
bridge = Path.Combine(folder, addinBridge);
if (File.Exists(bridge))
return bridge;
}
}
// Check in Visual Studio.app/
// In that case the name of the addin is used
bridge = Path.Combine(externalEditor, "Contents/Resources/lib/monodevelop/AddIns/" + addinName + "/" + addinBridge);
if (File.Exists(bridge))
return bridge;
return null;
}
}
}

View File

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

8
Editor/VSWhere.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b17896803f77494da73d73448fb6cb4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Editor/VSWhere/vswhere.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 585c3fb85b32bd64e8814074e754163e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
{
"name": "Unity.VisualStudio.Editor",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b93f844d45cfcc44fa2b0eed5c9ec6bb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Unity Technologies
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
LICENSE.md.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1fe9e6e1ae93cf64bbdfbd29300752e1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

31
ThirdPartyNotices.md Normal file
View File

@@ -0,0 +1,31 @@
This package contains third-party software components governed by the license(s) indicated below:
---------
Component Name: VSWhere
License Type: "MIT"
The MIT License (MIT)
Copyright (C) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---------
Component Name: benbuck/EnvDTE
License Type: Zero-Clause BSD
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7809f42f64c877f4b90ac233e5ec2372
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"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": "1.0.2",
"unity": "2019.2",
"unityRelease": "0a7",
"dependencies": {},
"relatedPackages": {
"com.unity.ide.visualstudio.tests": "1.0.2"
},
"repository": {
"type": "git",
"url": "git@github.cds.internal.unity3d.com:unity/com.unity.ide.visualstudio.git",
"revision": "34da9a734db5b16dfe722ac5371dac8c09099597"
}
}

7
package.json.meta Normal file
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 656f74f6edaa396458412b9e3903d8cd
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: