RiderScriptEditor.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using JetBrains.Annotations;
  6. using JetBrains.Rider.PathLocator;
  7. using Packages.Rider.Editor.ProjectGeneration;
  8. using Packages.Rider.Editor.Util;
  9. using Unity.CodeEditor;
  10. using UnityEditor;
  11. using UnityEngine;
  12. using Debug = UnityEngine.Debug;
  13. using OperatingSystemFamily = UnityEngine.OperatingSystemFamily;
  14. namespace Packages.Rider.Editor
  15. {
  16. [InitializeOnLoad]
  17. internal class RiderScriptEditor : IExternalCodeEditor
  18. {
  19. IDiscovery m_Discoverability;
  20. static IGenerator m_ProjectGeneration;
  21. RiderInitializer m_Initiliazer = new RiderInitializer();
  22. static RiderScriptEditor m_RiderScriptEditor;
  23. static RiderScriptEditor()
  24. {
  25. try
  26. {
  27. // todo: make ProjectGeneration lazy
  28. var projectGeneration = new ProjectGeneration.ProjectGeneration();
  29. m_RiderScriptEditor = new RiderScriptEditor(new Discovery(), projectGeneration);
  30. // preserve the order here, otherwise on startup, project generation Sync would happen multiple times
  31. CodeEditor.Register(m_RiderScriptEditor);
  32. InitializeInternal(CurrentEditor);
  33. // end of "preserve the order here"
  34. }
  35. catch (Exception e)
  36. {
  37. Debug.LogException(e);
  38. }
  39. }
  40. private static void ShowWarningOnUnexpectedScriptEditor(string path)
  41. {
  42. // Show warning, when Unity was started from Rider, but external editor is different https://github.com/JetBrains/resharper-unity/issues/1127
  43. try
  44. {
  45. var args = Environment.GetCommandLineArgs();
  46. var commandlineParser = new CommandLineParser(args);
  47. if (commandlineParser.Options.ContainsKey("-riderPath"))
  48. {
  49. var originRiderPath = commandlineParser.Options["-riderPath"];
  50. var originRealPath = GetEditorRealPath(originRiderPath);
  51. var originVersion = Discovery.RiderPathLocator.GetBuildNumber(originRealPath);
  52. var version = Discovery.RiderPathLocator.GetBuildNumber(path);
  53. if (originVersion != null && originVersion != version)
  54. {
  55. Debug.LogWarning("Unity was started by a version of Rider that is not the current default external editor. Advanced integration features cannot be enabled.");
  56. Debug.Log($"Unity was started by Rider {originVersion}, but external editor is set to: {path}");
  57. }
  58. }
  59. }
  60. catch (Exception e)
  61. {
  62. Debug.LogException(e);
  63. }
  64. }
  65. internal static string GetEditorRealPath(string path)
  66. {
  67. if (string.IsNullOrEmpty(path))
  68. return path;
  69. if (!FileSystemUtil.EditorPathExists(path))
  70. return path;
  71. if (SystemInfo.operatingSystemFamily != OperatingSystemFamily.Windows)
  72. {
  73. var realPath = FileSystemUtil.GetFinalPathName(path);
  74. // case of snap installation
  75. if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Linux)
  76. {
  77. if (new FileInfo(path).Name.ToLowerInvariant() == "rider" &&
  78. new FileInfo(realPath).Name.ToLowerInvariant() == "snap")
  79. {
  80. var snapInstallPath = "/snap/rider/current/bin/rider.sh";
  81. if (new FileInfo(snapInstallPath).Exists)
  82. return snapInstallPath;
  83. }
  84. }
  85. // in case of symlink
  86. return realPath;
  87. }
  88. return new FileInfo(path).FullName;
  89. }
  90. public RiderScriptEditor(IDiscovery discovery, IGenerator projectGeneration)
  91. {
  92. m_Discoverability = discovery;
  93. m_ProjectGeneration = projectGeneration;
  94. }
  95. public void OnGUI()
  96. {
  97. GUILayout.BeginHorizontal();
  98. var style = GUI.skin.label;
  99. var text = "Customize handled extensions in";
  100. EditorGUILayout.LabelField(text, style, GUILayout.Width(style.CalcSize(new GUIContent(text)).x));
  101. if (PluginSettings.LinkButton("Project Settings | Editor | Additional extensions to include"))
  102. {
  103. SettingsService.OpenProjectSettings("Project/Editor"); // how do I focus "Additional extensions to include"?
  104. }
  105. GUILayout.EndHorizontal();
  106. EditorGUILayout.LabelField("Generate .csproj files for:");
  107. EditorGUI.indentLevel++;
  108. SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "");
  109. SettingsButton(ProjectGenerationFlag.Local, "Local packages", "");
  110. SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "");
  111. SettingsButton(ProjectGenerationFlag.Git, "Git packages", "");
  112. SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "");
  113. #if UNITY_2019_3_OR_NEWER
  114. SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "");
  115. #endif
  116. SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "");
  117. SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'");
  118. RegenerateProjectFiles();
  119. EditorGUI.indentLevel--;
  120. }
  121. void RegenerateProjectFiles()
  122. {
  123. var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] {}));
  124. rect.width = 252;
  125. if (GUI.Button(rect, "Regenerate project files"))
  126. {
  127. m_ProjectGeneration.Sync();
  128. }
  129. }
  130. void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip)
  131. {
  132. var prevValue = m_ProjectGeneration.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
  133. var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
  134. if (newValue != prevValue)
  135. {
  136. m_ProjectGeneration.AssemblyNameProvider.ToggleProjectGeneration(preference);
  137. }
  138. }
  139. public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles,
  140. string[] importedFiles)
  141. {
  142. m_ProjectGeneration.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles),
  143. importedFiles);
  144. }
  145. public void SyncAll()
  146. {
  147. m_ProjectGeneration.Sync();
  148. }
  149. [UsedImplicitly]
  150. public static void SyncSolution() // generate-the-sln-file-via-script-or-command-line
  151. {
  152. m_ProjectGeneration.Sync();
  153. }
  154. [UsedImplicitly] // called from Rider EditorPlugin with reflection
  155. public static void SyncIfNeeded(bool checkProjectFiles)
  156. {
  157. AssetDatabase.Refresh();
  158. m_ProjectGeneration.SyncIfNeeded(new string[] { }, new string[] { }, checkProjectFiles);
  159. }
  160. [UsedImplicitly]
  161. public static void SyncSolutionAndOpenExternalEditor()
  162. {
  163. m_ProjectGeneration.Sync();
  164. CodeEditor.CurrentEditor.OpenProject();
  165. }
  166. /// <summary>
  167. /// In 2020.x is called each time ExternalEditor is changed
  168. /// In 2021.x+ is called each time ExternalEditor is changed and also on each appdomain reload
  169. /// </summary>
  170. /// <param name="editorInstallationPath"></param>
  171. public void Initialize(string editorInstallationPath)
  172. {
  173. var prevEditorVersion = RiderScriptEditorData.instance.prevEditorBuildNumber.ToVersion();
  174. RiderScriptEditorData.instance.Invalidate(editorInstallationPath, true);
  175. // previous editor did not have EditorPlugin
  176. // just load the EditorPlugin
  177. if (EditorPluginInterop.EditorPluginAssembly == null)
  178. {
  179. InitializeInternal(editorInstallationPath);
  180. return;
  181. }
  182. // previous editor was Rider with a different version
  183. // need to load new Editor plugin
  184. if (prevEditorVersion != null && prevEditorVersion != RiderScriptEditorData.instance.editorBuildNumber.ToVersion()) // in Unity 2019.3 any change in preference causes `Initialize` call
  185. {
  186. #if UNITY_2019_3_OR_NEWER
  187. EditorUtility.RequestScriptReload(); // EditorPlugin would get loaded
  188. #else
  189. UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
  190. #endif
  191. }
  192. }
  193. private static void InitializeInternal(string currentEditorPath)
  194. {
  195. var path = GetEditorRealPath(currentEditorPath);
  196. if (IsRiderOrFleetInstallation(path))
  197. {
  198. var installations = new HashSet<RiderPathLocator.RiderInfo>();
  199. if (RiderScriptEditorData.instance.installations != null)
  200. {
  201. foreach (var info in RiderScriptEditorData.instance.installations)
  202. {
  203. installations.Add(info);
  204. }
  205. }
  206. if (!RiderScriptEditorData.instance.initializedOnce || !FileSystemUtil.EditorPathExists(path))
  207. {
  208. foreach (var item in Discovery.RiderPathLocator.GetAllRiderPaths())
  209. {
  210. installations.Add(item);
  211. }
  212. // is likely outdated
  213. if (installations.All(a => GetEditorRealPath(a.Path) != path))
  214. {
  215. if (Discovery.RiderPathLocator.GetIsToolbox(path)) // is toolbox 1.x - update
  216. {
  217. var toolboxInstallations = installations.Where(a => a.IsToolbox).ToArray();
  218. if (toolboxInstallations.Any())
  219. {
  220. var newEditor = toolboxInstallations.OrderBy(a => a.BuildNumber).Last().Path;
  221. CodeEditor.SetExternalScriptEditor(newEditor);
  222. path = newEditor;
  223. }
  224. else if (installations.Any())
  225. {
  226. var newEditor = installations.OrderBy(a => a.BuildNumber).Last().Path;
  227. CodeEditor.SetExternalScriptEditor(newEditor);
  228. path = newEditor;
  229. }
  230. }
  231. else if (installations.Any()) // is non toolbox 1.x
  232. {
  233. if (!FileSystemUtil.EditorPathExists(path)) // previously used rider was removed
  234. {
  235. var newEditor = installations.OrderBy(a => a.BuildNumber).Last().Path;
  236. CodeEditor.SetExternalScriptEditor(newEditor);
  237. path = newEditor;
  238. }
  239. else // notify
  240. {
  241. var newEditorName = installations.OrderBy(a => a.BuildNumber).Last().Presentation;
  242. Debug.LogWarning($"Consider updating External Editor in Unity to {newEditorName}.");
  243. }
  244. }
  245. }
  246. ShowWarningOnUnexpectedScriptEditor(path);
  247. RiderScriptEditorData.instance.initializedOnce = true;
  248. }
  249. if (FileSystemUtil.EditorPathExists(path) && installations.All(a => a.Path != path)) // custom location
  250. {
  251. var info = new RiderPathLocator.RiderInfo(Discovery.RiderPathLocator, path, Discovery.RiderPathLocator.GetIsToolbox(path));
  252. installations.Add(info);
  253. }
  254. RiderScriptEditorData.instance.installations = installations.ToArray();
  255. RiderScriptEditorData.instance.Init();
  256. m_RiderScriptEditor.CreateSolutionIfDoesntExist();
  257. if (RiderScriptEditorData.instance.shouldLoadEditorPlugin)
  258. {
  259. m_RiderScriptEditor.m_Initiliazer.Initialize(path);
  260. }
  261. // can't switch to non-deprecated api, because UnityEditor.Build.BuildPipelineInterfaces.processors is internal
  262. #pragma warning disable 618
  263. EditorUserBuildSettings.activeBuildTargetChanged += () =>
  264. #pragma warning restore 618
  265. {
  266. RiderScriptEditorData.instance.hasChanges = true;
  267. };
  268. }
  269. }
  270. public bool OpenProject(string path, int line, int column)
  271. {
  272. var projectGeneration = (ProjectGeneration.ProjectGeneration) m_ProjectGeneration;
  273. // Assets - Open C# Project passes empty path here
  274. if (path != "" && !projectGeneration.HasValidExtension(path))
  275. {
  276. return false;
  277. }
  278. //if (!IsUnityScript(path))
  279. //{
  280. // m_ProjectGeneration.SyncIfNeeded(affectedFiles: new string[] { }, new string[] { });
  281. // var fastOpenResult = EditorPluginInterop.OpenFileDllImplementation(path, line, column);
  282. // if (fastOpenResult)
  283. // return true;
  284. //}
  285. var slnFile = GetSolutionFile(path);
  286. return Discovery.RiderFileOpener.OpenFile(CurrentEditor, slnFile, path, line, column);
  287. }
  288. private string GetSolutionFile(string path)
  289. {
  290. //if (IsUnityScript(path))
  291. //{
  292. // return Path.Combine(GetBaseUnityDeveloperFolder(), "Projects/CSharp/Unity.CSharpProjects.gen.sln");
  293. //}
  294. //
  295. //var solutionFile = m_ProjectGeneration.SolutionFile();
  296. //if (File.Exists(solutionFile))
  297. //{
  298. // return solutionFile;
  299. //}
  300. ET.CopyETslnHelper.Run();
  301. return "ET.sln";
  302. }
  303. static bool IsUnityScript(string path)
  304. {
  305. if (UnityEditor.Unsupported.IsDeveloperBuild())
  306. {
  307. var baseFolder = GetBaseUnityDeveloperFolder().Replace("\\", "/");
  308. var lowerPath = path.ToLowerInvariant().Replace("\\", "/");
  309. if (lowerPath.Contains((baseFolder + "/Runtime").ToLowerInvariant())
  310. || lowerPath.Contains((baseFolder + "/Editor").ToLowerInvariant()))
  311. {
  312. return true;
  313. }
  314. }
  315. return false;
  316. }
  317. static string GetBaseUnityDeveloperFolder()
  318. {
  319. return Directory.GetParent(EditorApplication.applicationPath).Parent.Parent.FullName;
  320. }
  321. public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
  322. {
  323. installation = default;
  324. if (string.IsNullOrEmpty(editorPath)) return false;
  325. if (FileSystemUtil.EditorPathExists(editorPath) && IsRiderOrFleetInstallation(editorPath))
  326. {
  327. if (RiderScriptEditorData.instance.installations == null) // the case when other CodeEditor is set from the very Unity start
  328. {
  329. RiderScriptEditorData.instance.installations = Discovery.RiderPathLocator.GetAllRiderPaths();
  330. }
  331. var realPath = GetEditorRealPath(editorPath);
  332. var editor = RiderScriptEditorData.instance.installations.FirstOrDefault(a => GetEditorRealPath(a.Path) == realPath);
  333. if (editor.Path != null)
  334. {
  335. installation = new CodeEditor.Installation
  336. {
  337. Name = editor.Presentation,
  338. Path = editor.Path
  339. };
  340. return true;
  341. }
  342. installation = new CodeEditor.Installation
  343. {
  344. Name = "Rider (custom location)",
  345. Path = editorPath
  346. };
  347. return true;
  348. }
  349. return false;
  350. }
  351. public static bool IsRiderOrFleetInstallation(string path)
  352. {
  353. if (IsAssetImportWorkerProcess())
  354. return false;
  355. #if UNITY_2021_1_OR_NEWER
  356. if (UnityEditor.MPE.ProcessService.level == UnityEditor.MPE.ProcessLevel.Secondary)
  357. return false;
  358. #elif UNITY_2020_2_OR_NEWER
  359. if (UnityEditor.MPE.ProcessService.level == UnityEditor.MPE.ProcessLevel.Slave)
  360. return false;
  361. #elif UNITY_2020_1_OR_NEWER
  362. if (Unity.MPE.ProcessService.level == Unity.MPE.ProcessLevel.UMP_SLAVE)
  363. return false;
  364. #endif
  365. if (string.IsNullOrEmpty(path))
  366. return false;
  367. return ExecutableStartsWith(path, "rider") || ExecutableStartsWith(path, "fleet");
  368. }
  369. public static bool ExecutableStartsWith(string path, string input)
  370. {
  371. var fileInfo = new FileInfo(path);
  372. var filename = fileInfo.Name;
  373. return filename.StartsWith(input, StringComparison.OrdinalIgnoreCase);
  374. }
  375. private static bool IsAssetImportWorkerProcess()
  376. {
  377. #if UNITY_2020_2_OR_NEWER
  378. return UnityEditor.AssetDatabase.IsAssetImportWorkerProcess();
  379. #elif UNITY_2019_3_OR_NEWER
  380. return UnityEditor.Experimental.AssetDatabaseExperimental.IsAssetImportWorkerProcess();
  381. #else
  382. return false;
  383. #endif
  384. }
  385. public static string CurrentEditor // works fast, doesn't validate if executable really exists
  386. => EditorPrefs.GetString("kScriptsDefaultApp");
  387. public CodeEditor.Installation[] Installations => m_Discoverability.PathCallback();
  388. private void CreateSolutionIfDoesntExist()
  389. {
  390. if (!m_ProjectGeneration.HasSolutionBeenGenerated())
  391. {
  392. m_ProjectGeneration.Sync();
  393. }
  394. }
  395. }
  396. }