2023-03-28 13:24:16 -04:00
using System ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using JetBrains.Annotations ;
using Packages.Rider.Editor.ProjectGeneration ;
using Packages.Rider.Editor.Util ;
using Unity.CodeEditor ;
using UnityEditor ;
using UnityEngine ;
using Debug = UnityEngine . Debug ;
namespace Packages.Rider.Editor
{
[InitializeOnLoad]
internal class RiderScriptEditor : IExternalCodeEditor
{
IDiscovery m_Discoverability ;
static IGenerator m_ProjectGeneration ;
RiderInitializer m_Initiliazer = new RiderInitializer ( ) ;
2023-05-07 18:43:11 -04:00
static RiderScriptEditor m_RiderScriptEditor ;
2023-03-28 13:24:16 -04:00
static RiderScriptEditor ( )
{
try
{
// todo: make ProjectGeneration lazy
var projectGeneration = new ProjectGeneration . ProjectGeneration ( ) ;
2023-05-07 18:43:11 -04:00
m_RiderScriptEditor = new RiderScriptEditor ( new Discovery ( ) , projectGeneration ) ;
CodeEditor . Register ( m_RiderScriptEditor ) ;
2023-03-28 13:24:16 -04:00
2023-05-07 18:43:11 -04:00
InitializeInternal ( CurrentEditor ) ;
2023-03-28 13:24:16 -04:00
}
catch ( Exception e )
{
Debug . LogException ( e ) ;
}
}
private static void ShowWarningOnUnexpectedScriptEditor ( string path )
{
// Show warning, when Unity was started from Rider, but external editor is different https://github.com/JetBrains/resharper-unity/issues/1127
try
{
var args = Environment . GetCommandLineArgs ( ) ;
var commandlineParser = new CommandLineParser ( args ) ;
if ( commandlineParser . Options . ContainsKey ( "-riderPath" ) )
{
var originRiderPath = commandlineParser . Options [ "-riderPath" ] ;
var originRealPath = GetEditorRealPath ( originRiderPath ) ;
var originVersion = RiderPathLocator . GetBuildNumber ( originRealPath ) ;
var version = RiderPathLocator . GetBuildNumber ( path ) ;
if ( originVersion ! = null & & originVersion ! = version )
{
Debug . LogWarning ( "Unity was started by a version of Rider that is not the current default external editor. Advanced integration features cannot be enabled." ) ;
Debug . Log ( $"Unity was started by Rider {originVersion}, but external editor is set to: {path}" ) ;
}
}
}
catch ( Exception e )
{
Debug . LogException ( e ) ;
}
}
internal static string GetEditorRealPath ( string path )
{
if ( string . IsNullOrEmpty ( path ) )
{
return path ;
}
if ( ! FileSystemUtil . EditorPathExists ( path ) )
return path ;
if ( SystemInfo . operatingSystemFamily ! = OperatingSystemFamily . Windows )
{
var realPath = FileSystemUtil . GetFinalPathName ( path ) ;
// case of snap installation
if ( SystemInfo . operatingSystemFamily = = OperatingSystemFamily . Linux )
{
if ( new FileInfo ( path ) . Name . ToLowerInvariant ( ) = = "rider" & &
new FileInfo ( realPath ) . Name . ToLowerInvariant ( ) = = "snap" )
{
var snapInstallPath = "/snap/rider/current/bin/rider.sh" ;
if ( new FileInfo ( snapInstallPath ) . Exists )
return snapInstallPath ;
}
}
// in case of symlink
return realPath ;
}
return path ;
}
public RiderScriptEditor ( IDiscovery discovery , IGenerator projectGeneration )
{
m_Discoverability = discovery ;
m_ProjectGeneration = projectGeneration ;
}
public void OnGUI ( )
{
2023-05-07 18:43:11 -04:00
GUILayout . BeginHorizontal ( ) ;
2023-03-28 13:24:16 -04:00
2023-05-07 18:43:11 -04:00
var style = GUI . skin . label ;
var text = "Customize handled extensions in" ;
EditorGUILayout . LabelField ( text , style , GUILayout . Width ( style . CalcSize ( new GUIContent ( text ) ) . x ) ) ;
if ( PluginSettings . LinkButton ( "Project Settings | Editor | Additional extensions to include" ) )
{
SettingsService . OpenProjectSettings ( "Project/Editor" ) ; // how do I focus "Additional extensions to include"?
2023-03-28 13:24:16 -04:00
}
2023-05-07 18:43:11 -04:00
GUILayout . EndHorizontal ( ) ;
2023-03-28 13:24:16 -04: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" , "" ) ;
#if UNITY_2019_3_OR_NEWER
SettingsButton ( ProjectGenerationFlag . LocalTarBall , "Local tarball" , "" ) ;
#endif
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 ( )
{
var rect = EditorGUI . IndentedRect ( EditorGUILayout . GetControlRect ( new GUILayoutOption [ ] { } ) ) ;
rect . width = 252 ;
if ( GUI . Button ( rect , "Regenerate project files" ) )
{
m_ProjectGeneration . Sync ( ) ;
}
}
void SettingsButton ( ProjectGenerationFlag preference , string guiMessage , string toolTip )
{
var prevValue = m_ProjectGeneration . AssemblyNameProvider . ProjectGenerationFlag . HasFlag ( preference ) ;
var newValue = EditorGUILayout . Toggle ( new GUIContent ( guiMessage , toolTip ) , prevValue ) ;
if ( newValue ! = prevValue )
{
m_ProjectGeneration . AssemblyNameProvider . ToggleProjectGeneration ( preference ) ;
}
}
public void SyncIfNeeded ( string [ ] addedFiles , string [ ] deletedFiles , string [ ] movedFiles , string [ ] movedFromFiles ,
string [ ] importedFiles )
{
m_ProjectGeneration . SyncIfNeeded ( addedFiles . Union ( deletedFiles ) . Union ( movedFiles ) . Union ( movedFromFiles ) ,
importedFiles ) ;
}
public void SyncAll ( )
{
AssetDatabase . Refresh ( ) ;
m_ProjectGeneration . SyncIfNeeded ( new string [ ] { } , new string [ ] { } ) ;
}
[UsedImplicitly]
public static void SyncSolution ( ) // generate-the-sln-file-via-script-or-command-line
{
m_ProjectGeneration . Sync ( ) ;
}
[UsedImplicitly] // called from Rider EditorPlugin with reflection
public static void SyncIfNeeded ( bool checkProjectFiles )
{
AssetDatabase . Refresh ( ) ;
m_ProjectGeneration . SyncIfNeeded ( new string [ ] { } , new string [ ] { } , checkProjectFiles ) ;
}
[UsedImplicitly]
public static void SyncSolutionAndOpenExternalEditor ( )
{
m_ProjectGeneration . Sync ( ) ;
CodeEditor . CurrentEditor . OpenProject ( ) ;
}
public void Initialize ( string editorInstallationPath ) // is called each time ExternalEditor is changed
{
var prevEditorBuildNumber = RiderScriptEditorData . instance . prevEditorBuildNumber ;
RiderScriptEditorData . instance . Invalidate ( editorInstallationPath , true ) ;
2023-05-07 18:43:11 -04:00
if ( EditorPluginInterop . EditorPluginAssembly = = null ) // no need to reload all - just load the EditorPlugin
{
InitializeInternal ( editorInstallationPath ) ;
return ;
}
2023-03-28 13:24:16 -04:00
if ( prevEditorBuildNumber . ToVersion ( ) ! = RiderScriptEditorData . instance . editorBuildNumber . ToVersion ( ) ) // in Unity 2019.3 any change in preference causes `Initialize` call
{
m_ProjectGeneration . Sync ( ) ; // regenerate csproj and sln for new editor
#if UNITY_2019_3_OR_NEWER
EditorUtility . RequestScriptReload ( ) ; // EditorPlugin would get loaded
#else
UnityEditorInternal . InternalEditorUtility . RequestScriptReload ( ) ;
#endif
}
}
2023-05-07 18:43:11 -04:00
private static void InitializeInternal ( string currentEditorPath )
{
var path = GetEditorRealPath ( currentEditorPath ) ;
if ( IsRiderOrFleetInstallation ( path ) )
{
RiderPathLocator . RiderInfo [ ] installations = null ;
if ( ! RiderScriptEditorData . instance . initializedOnce )
{
installations = RiderPathLocator . GetAllRiderPaths ( ) . OrderBy ( a = > a . BuildNumber ) . ToArray ( ) ;
// is likely outdated
if ( installations . Any ( ) & & installations . All ( a = > GetEditorRealPath ( a . Path ) ! = path ) )
{
if ( RiderPathLocator . GetIsToolbox ( path ) ) // is toolbox - update
{
var toolboxInstallations = installations . Where ( a = > a . IsToolbox ) . ToArray ( ) ;
if ( toolboxInstallations . Any ( ) )
{
var newEditor = toolboxInstallations . Last ( ) . Path ;
CodeEditor . SetExternalScriptEditor ( newEditor ) ;
path = newEditor ;
}
else
{
var newEditor = installations . Last ( ) . Path ;
CodeEditor . SetExternalScriptEditor ( newEditor ) ;
path = newEditor ;
}
}
else // is non toolbox - notify
{
var newEditorName = installations . Last ( ) . Presentation ;
Debug . LogWarning ( $"Consider updating External Editor in Unity to {newEditorName}." ) ;
}
}
ShowWarningOnUnexpectedScriptEditor ( path ) ;
RiderScriptEditorData . instance . initializedOnce = true ;
}
if ( ! FileSystemUtil . EditorPathExists ( path ) ) // previously used rider was removed
{
if ( installations = = null )
installations = RiderPathLocator . GetAllRiderPaths ( ) . OrderBy ( a = > a . BuildNumber ) . ToArray ( ) ;
if ( installations . Any ( ) )
{
var newEditor = installations . Last ( ) . Path ;
CodeEditor . SetExternalScriptEditor ( newEditor ) ;
path = newEditor ;
}
}
RiderScriptEditorData . instance . Init ( ) ;
m_RiderScriptEditor . CreateSolutionIfDoesntExist ( ) ;
if ( RiderScriptEditorData . instance . shouldLoadEditorPlugin )
{
m_RiderScriptEditor . m_Initiliazer . Initialize ( path ) ;
}
// can't switch to non-deprecated api, because UnityEditor.Build.BuildPipelineInterfaces.processors is internal
#pragma warning disable 618
EditorUserBuildSettings . activeBuildTargetChanged + = ( ) = >
#pragma warning restore 618
{
RiderScriptEditorData . instance . hasChanges = true ;
} ;
}
}
2023-03-28 13:24:16 -04:00
public bool OpenProject ( string path , int line , int column )
{
var projectGeneration = ( ProjectGeneration . ProjectGeneration ) m_ProjectGeneration ;
// Assets - Open C# Project passes empty path here
if ( path ! = "" & & ! projectGeneration . HasValidExtension ( path ) )
{
return false ;
}
if ( ! IsUnityScript ( path ) )
{
m_ProjectGeneration . SyncIfNeeded ( affectedFiles : new string [ ] { } , new string [ ] { } ) ;
var fastOpenResult = EditorPluginInterop . OpenFileDllImplementation ( path , line , column ) ;
if ( fastOpenResult )
return true ;
}
2023-05-07 18:43:11 -04:00
var solution = GetSolutionFile ( path ) ; // TODO: If solution file doesn't exist resync.
if ( ExecutableStartsWith ( CodeEditor . CurrentEditorInstallation , "fleet" ) ) // I hope fleet would provide a way to specify the sln
solution = new FileInfo ( solution ) . Directory . FullName ;
2023-03-28 13:24:16 -04:00
if ( SystemInfo . operatingSystemFamily = = OperatingSystemFamily . MacOSX )
{
2023-05-07 18:43:11 -04:00
return OpenOSXApp ( solution , path , line , column ) ;
2023-03-28 13:24:16 -04:00
}
solution = solution = = "" ? "" : $"\" { solution } \ "" ;
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = CodeEditor . CurrentEditorInstallation ,
Arguments = $"{solution} -l {line} \" { path } \ "" ,
UseShellExecute = true ,
}
} ;
process . Start ( ) ;
return true ;
}
2023-05-07 18:43:11 -04:00
private bool OpenOSXApp ( string solution , string path , int line , int column )
2023-03-28 13:24:16 -04:00
{
solution = solution = = "" ? "" : $"\" { solution } \ "" ;
var pathArguments = path = = "" ? "" : $"-l {line} \" { path } \ "" ;
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "open" ,
Arguments = $"-n -j \" { CodeEditor . CurrentEditorInstallation } \ " --args {solution} {pathArguments}" ,
CreateNoWindow = true ,
UseShellExecute = true ,
}
} ;
process . Start ( ) ;
return true ;
}
private string GetSolutionFile ( string path )
{
if ( IsUnityScript ( path ) )
{
return Path . Combine ( GetBaseUnityDeveloperFolder ( ) , "Projects/CSharp/Unity.CSharpProjects.gen.sln" ) ;
}
var solutionFile = m_ProjectGeneration . SolutionFile ( ) ;
if ( File . Exists ( solutionFile ) )
{
return solutionFile ;
}
return "" ;
}
static bool IsUnityScript ( string path )
{
if ( UnityEditor . Unsupported . IsDeveloperBuild ( ) )
{
var baseFolder = GetBaseUnityDeveloperFolder ( ) . Replace ( "\\" , "/" ) ;
var lowerPath = path . ToLowerInvariant ( ) . Replace ( "\\" , "/" ) ;
if ( lowerPath . Contains ( ( baseFolder + "/Runtime" ) . ToLowerInvariant ( ) )
| | lowerPath . Contains ( ( baseFolder + "/Editor" ) . ToLowerInvariant ( ) ) )
{
return true ;
}
}
return false ;
}
static string GetBaseUnityDeveloperFolder ( )
{
return Directory . GetParent ( EditorApplication . applicationPath ) . Parent . Parent . FullName ;
}
public bool TryGetInstallationForPath ( string editorPath , out CodeEditor . Installation installation )
{
2023-05-07 18:43:11 -04:00
if ( FileSystemUtil . EditorPathExists ( editorPath ) & & IsRiderOrFleetInstallation ( editorPath ) )
2023-03-28 13:24:16 -04:00
{
2023-05-07 18:43:11 -04:00
var info = new RiderPathLocator . RiderInfo ( editorPath , RiderPathLocator . GetIsToolbox ( editorPath ) ) ;
2023-03-28 13:24:16 -04:00
installation = new CodeEditor . Installation
{
Name = info . Presentation ,
Path = info . Path
} ;
return true ;
}
installation = default ;
return false ;
}
2023-05-07 18:43:11 -04:00
public static bool IsRiderOrFleetInstallation ( string path )
2023-03-28 13:24:16 -04:00
{
if ( IsAssetImportWorkerProcess ( ) )
return false ;
#if UNITY_2021_1_OR_NEWER
if ( UnityEditor . MPE . ProcessService . level = = UnityEditor . MPE . ProcessLevel . Secondary )
return false ;
#elif UNITY_2020_2_OR_NEWER
if ( UnityEditor . MPE . ProcessService . level = = UnityEditor . MPE . ProcessLevel . Slave )
return false ;
#elif UNITY_2020_1_OR_NEWER
if ( Unity . MPE . ProcessService . level = = Unity . MPE . ProcessLevel . UMP_SLAVE )
return false ;
#endif
if ( string . IsNullOrEmpty ( path ) )
return false ;
2023-05-07 18:43:11 -04:00
return ExecutableStartsWith ( path , "rider" ) | | ExecutableStartsWith ( path , "fleet" ) ;
}
public static bool ExecutableStartsWith ( string path , string input )
{
2023-03-28 13:24:16 -04:00
var fileInfo = new FileInfo ( path ) ;
2023-05-07 18:43:11 -04:00
var filename = fileInfo . Name ;
return filename . StartsWith ( input , StringComparison . OrdinalIgnoreCase ) ;
}
public static string GetProductNameForPresentation ( FileInfo path )
{
var filename = path . Name ;
if ( filename . StartsWith ( "rider" , StringComparison . OrdinalIgnoreCase ) )
return "Rider" ;
if ( filename . StartsWith ( "fleet" , StringComparison . OrdinalIgnoreCase ) )
return "Fleet" ;
return filename ;
2023-03-28 13:24:16 -04:00
}
private static bool IsAssetImportWorkerProcess ( )
{
#if UNITY_2020_2_OR_NEWER
return UnityEditor . AssetDatabase . IsAssetImportWorkerProcess ( ) ;
#elif UNITY_2019_3_OR_NEWER
return UnityEditor . Experimental . AssetDatabaseExperimental . IsAssetImportWorkerProcess ( ) ;
#else
return false ;
#endif
}
public static string CurrentEditor // works fast, doesn't validate if executable really exists
= > EditorPrefs . GetString ( "kScriptsDefaultApp" ) ;
public CodeEditor . Installation [ ] Installations = > m_Discoverability . PathCallback ( ) ;
private void CreateSolutionIfDoesntExist ( )
{
if ( ! m_ProjectGeneration . HasSolutionBeenGenerated ( ) )
{
m_ProjectGeneration . Sync ( ) ;
}
}
}
}