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

RiderScriptEditor.cs 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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. var solutionFile = m_ProjectGeneration.SolutionFile();
  295. if (File.Exists(solutionFile))
  296. {
  297. return solutionFile;
  298. }
  299. return "";
  300. }
  301. static bool IsUnityScript(string path)
  302. {
  303. if (UnityEditor.Unsupported.IsDeveloperBuild())
  304. {
  305. var baseFolder = GetBaseUnityDeveloperFolder().Replace("\\", "/");
  306. var lowerPath = path.ToLowerInvariant().Replace("\\", "/");
  307. if (lowerPath.Contains((baseFolder + "/Runtime").ToLowerInvariant())
  308. || lowerPath.Contains((baseFolder + "/Editor").ToLowerInvariant()))
  309. {
  310. return true;
  311. }
  312. }
  313. return false;
  314. }
  315. static string GetBaseUnityDeveloperFolder()
  316. {
  317. return Directory.GetParent(EditorApplication.applicationPath).Parent.Parent.FullName;
  318. }
  319. public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
  320. {
  321. installation = default;
  322. if (string.IsNullOrEmpty(editorPath)) return false;
  323. if (FileSystemUtil.EditorPathExists(editorPath) && IsRiderOrFleetInstallation(editorPath))
  324. {
  325. if (RiderScriptEditorData.instance.installations == null) // the case when other CodeEditor is set from the very Unity start
  326. {
  327. RiderScriptEditorData.instance.installations = Discovery.RiderPathLocator.GetAllRiderPaths();
  328. }
  329. var realPath = GetEditorRealPath(editorPath);
  330. var editor = RiderScriptEditorData.instance.installations.FirstOrDefault(a => GetEditorRealPath(a.Path) == realPath);
  331. if (editor.Path != null)
  332. {
  333. installation = new CodeEditor.Installation
  334. {
  335. Name = editor.Presentation,
  336. Path = editor.Path
  337. };
  338. return true;
  339. }
  340. installation = new CodeEditor.Installation
  341. {
  342. Name = "Rider (custom location)",
  343. Path = editorPath
  344. };
  345. return true;
  346. }
  347. return false;
  348. }
  349. public static bool IsRiderOrFleetInstallation(string path)
  350. {
  351. if (IsAssetImportWorkerProcess())
  352. return false;
  353. #if UNITY_2021_1_OR_NEWER
  354. if (UnityEditor.MPE.ProcessService.level == UnityEditor.MPE.ProcessLevel.Secondary)
  355. return false;
  356. #elif UNITY_2020_2_OR_NEWER
  357. if (UnityEditor.MPE.ProcessService.level == UnityEditor.MPE.ProcessLevel.Slave)
  358. return false;
  359. #elif UNITY_2020_1_OR_NEWER
  360. if (Unity.MPE.ProcessService.level == Unity.MPE.ProcessLevel.UMP_SLAVE)
  361. return false;
  362. #endif
  363. if (string.IsNullOrEmpty(path))
  364. return false;
  365. return ExecutableStartsWith(path, "rider") || ExecutableStartsWith(path, "fleet");
  366. }
  367. public static bool ExecutableStartsWith(string path, string input)
  368. {
  369. var fileInfo = new FileInfo(path);
  370. var filename = fileInfo.Name;
  371. return filename.StartsWith(input, StringComparison.OrdinalIgnoreCase);
  372. }
  373. private static bool IsAssetImportWorkerProcess()
  374. {
  375. #if UNITY_2020_2_OR_NEWER
  376. return UnityEditor.AssetDatabase.IsAssetImportWorkerProcess();
  377. #elif UNITY_2019_3_OR_NEWER
  378. return UnityEditor.Experimental.AssetDatabaseExperimental.IsAssetImportWorkerProcess();
  379. #else
  380. return false;
  381. #endif
  382. }
  383. public static string CurrentEditor // works fast, doesn't validate if executable really exists
  384. => EditorPrefs.GetString("kScriptsDefaultApp");
  385. public CodeEditor.Installation[] Installations => m_Discoverability.PathCallback();
  386. private void CreateSolutionIfDoesntExist()
  387. {
  388. if (!m_ProjectGeneration.HasSolutionBeenGenerated())
  389. {
  390. m_ProjectGeneration.Sync();
  391. }
  392. }
  393. }
  394. }