2019-11-06 00:00:00 +00:00
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* 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 ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using UnityEditor ;
using UnityEngine ;
using Unity.CodeEditor ;
using System.Runtime.InteropServices ;
namespace Microsoft.Unity.VisualStudio.Editor
{
[InitializeOnLoad]
public class VisualStudioEditor : IExternalCodeEditor
{
2020-10-30 00:00:00 +00:00
private static readonly IVisualStudioInstallation [ ] _installations ;
2019-11-06 00:00:00 +00:00
internal static bool IsOSX = > Application . platform = = RuntimePlatform . OSXEditor ;
2020-10-30 00:00:00 +00:00
internal static bool IsWindows = > ! IsOSX & & Path . DirectorySeparatorChar = = FileUtility . WinSeparator & & Environment . NewLine = = "\r\n" ;
2019-11-06 00:00:00 +00:00
CodeEditor . Installation [ ] IExternalCodeEditor . Installations = > _installations
. Select ( i = > i . ToCodeEditorInstallation ( ) )
2020-10-30 00:00:00 +00:00
. ToArray ( ) ;
2019-11-06 00:00:00 +00:00
private readonly IGenerator _generator = new ProjectGeneration ( ) ;
static VisualStudioEditor ( )
{
try
{
_installations = Discovery
. GetVisualStudioInstallations ( )
. ToArray ( ) ;
}
catch ( Exception ex )
{
2020-10-30 00:00:00 +00:00
UnityEngine . Debug . LogError ( $"Error detecting Visual Studio installations: {ex}" ) ;
2019-11-06 00:00:00 +00:00
_installations = Array . Empty < VisualStudioInstallation > ( ) ;
}
CodeEditor . Register ( new VisualStudioEditor ( ) ) ;
}
2020-05-27 00:00:00 +00:00
internal static bool IsEnabled
{
get
{
return CodeEditor . CurrentEditor is VisualStudioEditor ;
}
}
2019-11-06 00:00:00 +00:00
public void CreateIfDoesntExist ( )
{
if ( ! _generator . HasSolutionBeenGenerated ( ) )
_generator . Sync ( ) ;
}
public void Initialize ( string editorInstallationPath )
{
}
2020-10-30 00:00:00 +00:00
internal virtual bool TryGetVisualStudioInstallationForPath ( string editorPath , out IVisualStudioInstallation installation )
2019-11-06 00:00:00 +00:00
{
// lookup for well known installations
foreach ( var candidate in _installations )
{
if ( ! string . Equals ( Path . GetFullPath ( editorPath ) , Path . GetFullPath ( candidate . Path ) , StringComparison . OrdinalIgnoreCase ) )
continue ;
installation = candidate ;
return true ;
}
return Discovery . TryDiscoverInstallation ( editorPath , out installation ) ;
}
2020-10-30 00:00:00 +00:00
public virtual bool TryGetInstallationForPath ( string editorPath , out CodeEditor . Installation installation )
2019-11-06 00:00:00 +00:00
{
var result = TryGetVisualStudioInstallationForPath ( editorPath , out var vsi ) ;
installation = vsi = = null ? default : vsi . ToCodeEditorInstallation ( ) ;
return result ;
}
public void OnGUI ( )
{
GUILayout . BeginHorizontal ( ) ;
GUILayout . FlexibleSpace ( ) ;
var package = UnityEditor . PackageManager . PackageInfo . FindForAssembly ( GetType ( ) . Assembly ) ;
var style = new GUIStyle
{
richText = true ,
margin = new RectOffset ( 0 , 4 , 0 , 0 )
} ;
GUILayout . Label ( $"<size=10><color=grey>{package.displayName} v{package.version} enabled</color></size>" , style ) ;
GUILayout . EndHorizontal ( ) ;
2020-03-19 00:00:00 +00:00
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 ( ) ;
EditorGUI . indentLevel - - ;
}
void RegenerateProjectFiles ( )
2020-10-30 00:00:00 +00:00
{
var rect = EditorGUI . IndentedRect ( EditorGUILayout . GetControlRect ( new GUILayoutOption [ ] { } ) ) ;
rect . width = 252 ;
if ( GUI . Button ( rect , "Regenerate project files" ) )
{
_generator . Sync ( ) ;
}
}
2020-03-19 00:00:00 +00:00
void SettingsButton ( ProjectGenerationFlag preference , string guiMessage , string toolTip )
{
var prevValue = _generator . AssemblyNameProvider . ProjectGenerationFlag . HasFlag ( preference ) ;
var newValue = EditorGUILayout . Toggle ( new GUIContent ( guiMessage , toolTip ) , prevValue ) ;
if ( newValue ! = prevValue )
2019-11-06 00:00:00 +00:00
{
2020-03-19 00:00:00 +00:00
_generator . AssemblyNameProvider . ToggleProjectGeneration ( preference ) ;
2019-11-06 00:00:00 +00:00
}
}
public void SyncIfNeeded ( string [ ] addedFiles , string [ ] deletedFiles , string [ ] movedFiles , string [ ] movedFromFiles , string [ ] importedFiles )
{
_generator . SyncIfNeeded ( addedFiles . Union ( deletedFiles ) . Union ( movedFiles ) . Union ( movedFromFiles ) , importedFiles ) ;
foreach ( var file in importedFiles . Where ( a = > Path . GetExtension ( a ) = = ".pdb" ) )
{
var pdbFile = FileUtility . GetAssetFullPath ( file ) ;
2020-03-19 00:00:00 +00:00
2020-10-30 00:00:00 +00:00
// skip Unity packages like com.unity.ext.nunit
if ( pdbFile . IndexOf ( $"{Path.DirectorySeparatorChar}com.unity." , StringComparison . OrdinalIgnoreCase ) > 0 )
continue ;
var asmFile = Path . ChangeExtension ( pdbFile , ".dll" ) ;
2020-03-19 00:00:00 +00:00
if ( ! File . Exists ( asmFile ) | | ! Image . IsAssembly ( asmFile ) )
continue ;
2019-11-06 00:00:00 +00:00
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." ) ;
}
}
public void SyncAll ( )
{
AssetDatabase . Refresh ( ) ;
_generator . Sync ( ) ;
}
bool IsSupportedPath ( string path )
{
// Path is empty with "Open C# Project", as we only want to open the solution without specific files
if ( string . IsNullOrEmpty ( path ) )
return true ;
// 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 ;
}
2020-10-30 00:00:00 +00:00
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." ) ;
}
2019-11-06 00:00:00 +00:00
public bool OpenProject ( string path , int line , int column )
{
2020-10-30 00:00:00 +00:00
CheckCurrentEditorInstallation ( ) ;
2019-11-06 00:00:00 +00:00
if ( ! IsSupportedPath ( path ) )
return false ;
2020-10-30 00:00:00 +00:00
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." ) ;
2019-11-06 00:00:00 +00:00
if ( IsOSX )
return OpenOSXApp ( path , line , column ) ;
if ( IsWindows )
return OpenWindowsApp ( path , line ) ;
return false ;
}
2020-10-30 00:00:00 +00:00
private static string GetProjectGenerationFlagDescription ( ProjectGenerationFlag flag )
{
switch ( flag )
{
case ProjectGenerationFlag . BuiltIn :
return "Built-in packages" ;
case ProjectGenerationFlag . Embedded :
return "Embedded packages" ;
case ProjectGenerationFlag . Git :
return "Git packages" ;
case ProjectGenerationFlag . Local :
return "Local packages" ;
case ProjectGenerationFlag . LocalTarBall :
return "Local tarball" ;
case ProjectGenerationFlag . PlayerAssemblies :
return "Player projects" ;
case ProjectGenerationFlag . Registry :
return "Registry packages" ;
case ProjectGenerationFlag . Unknown :
return "Packages from unknown sources" ;
case ProjectGenerationFlag . None :
default :
return string . Empty ;
}
}
private bool IsProjectGeneratedFor ( string path , out ProjectGenerationFlag missingFlag )
{
missingFlag = ProjectGenerationFlag . None ;
// No need to check when opening the whole solution
if ( string . IsNullOrEmpty ( path ) )
return true ;
// We only want to check for cs scripts
if ( ProjectGeneration . ScriptingLanguageFor ( path ) ! = ScriptingLanguage . CSharp )
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 )
. Replace ( basePath , string . Empty )
. Trim ( FileUtility . UnixSeparator ) ;
var packageInfo = UnityEditor . PackageManager . PackageInfo . FindForAssetPath ( relativePath ) ;
if ( packageInfo = = null )
return true ;
var source = packageInfo . source ;
if ( ! Enum . TryParse < ProjectGenerationFlag > ( source . ToString ( ) , out var flag ) )
return true ;
if ( _generator . AssemblyNameProvider . ProjectGenerationFlag . HasFlag ( flag ) )
return true ;
// Return false if we found a source not flagged for generation
missingFlag = flag ;
return false ;
}
2019-11-06 00:00:00 +00:00
private bool OpenWindowsApp ( string path , int line )
{
var progpath = FileUtility
. FindPackageAssetFullPath ( "COMIntegration a:packages" , "COMIntegration.exe" )
. FirstOrDefault ( ) ;
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 process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = progpath ,
Arguments = $"\" { CodeEditor . CurrentEditorInstallation } \ " \"{absolutePath}\" {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 ) )
{
Console . WriteLine ( "Error: \n" + errorOutput ) ;
}
process . WaitForExit ( ) ;
return result ;
}
[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 ) ;
}
string solution = GetOrGenerateSolutionFile ( path ) ;
return OpenVisualStudio ( CodeEditor . CurrentEditorInstallation , solution , absolutePath , line ) ;
}
private string GetOrGenerateSolutionFile ( string path )
{
var solution = GetSolutionFile ( path ) ;
if ( solution = = "" )
{
_generator . Sync ( ) ;
solution = GetSolutionFile ( path ) ;
}
return solution ;
}
string GetSolutionFile ( string path )
{
var solutionFile = _generator . SolutionFile ( ) ;
if ( File . Exists ( solutionFile ) )
{
return solutionFile ;
}
return "" ;
}
}
}