You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

468 lines
16 KiB
C#

1 year ago
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();
static RiderScriptEditor m_RiderScriptEditor;
static RiderScriptEditor()
{
try
{
// todo: make ProjectGeneration lazy
var projectGeneration = new ProjectGeneration.ProjectGeneration();
m_RiderScriptEditor = new RiderScriptEditor(new Discovery(), projectGeneration);
CodeEditor.Register(m_RiderScriptEditor);
InitializeInternal(CurrentEditor);
}
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()
{
GUILayout.BeginHorizontal();
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"?
}
GUILayout.EndHorizontal();
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();
}
/// <summary>
/// In 2020.x is called each time ExternalEditor is changed
/// In 2021.x+ is called each time ExternalEditor is changed and also on each appdomain reload
/// </summary>
/// <param name="editorInstallationPath"></param>
public void Initialize(string editorInstallationPath)
{
var prevEditorBuildNumber = RiderScriptEditorData.instance.prevEditorBuildNumber;
RiderScriptEditorData.instance.Invalidate(editorInstallationPath, true);
if (EditorPluginInterop.EditorPluginAssembly == null) // no need to reload all - just load the EditorPlugin
{
InitializeInternal(editorInstallationPath);
return;
}
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
}
}
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;
};
}
}
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;
}
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;
if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX)
{
return OpenOSXApp(solution, path, line, column);
}
solution = solution == "" ? "" : $"\"{solution}\"";
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = CodeEditor.CurrentEditorInstallation,
Arguments = $"{solution} -l {line} \"{path}\"",
UseShellExecute = true,
}
};
process.Start();
return true;
}
private bool OpenOSXApp(string solution, string path, int line, int column)
{
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)
{
if (FileSystemUtil.EditorPathExists(editorPath) && IsRiderOrFleetInstallation(editorPath))
{
var info = new RiderPathLocator.RiderInfo(editorPath, RiderPathLocator.GetIsToolbox(editorPath));
installation = new CodeEditor.Installation
{
Name = info.Presentation,
Path = info.Path
};
return true;
}
installation = default;
return false;
}
public static bool IsRiderOrFleetInstallation(string path)
{
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;
return ExecutableStartsWith(path, "rider") || ExecutableStartsWith(path, "fleet");
}
public static bool ExecutableStartsWith(string path, string input)
{
var fileInfo = new FileInfo(path);
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;
}
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();
}
}
}
}