RiderScriptEditor.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Packages.Rider.Editor.ProjectGeneration;
  7. using Packages.Rider.Editor.Util;
  8. using Unity.CodeEditor;
  9. using UnityEditor;
  10. using UnityEditor.Build;
  11. using UnityEngine;
  12. using Debug = UnityEngine.Debug;
  13. namespace Packages.Rider.Editor
  14. {
  15. [InitializeOnLoad]
  16. internal class RiderScriptEditor : IExternalCodeEditor
  17. {
  18. IDiscovery m_Discoverability;
  19. IGenerator m_ProjectGeneration;
  20. RiderInitializer m_Initiliazer = new RiderInitializer();
  21. static RiderScriptEditor()
  22. {
  23. try
  24. {
  25. // todo: make ProjectGeneration lazy
  26. var projectGeneration = new ProjectGeneration.ProjectGeneration();
  27. var editor = new RiderScriptEditor(new Discovery(), projectGeneration);
  28. CodeEditor.Register(editor);
  29. var path = GetEditorRealPath(CurrentEditor);
  30. if (IsRiderInstallation(path))
  31. {
  32. RiderPathLocator.RiderInfo[] installations = null;
  33. if (!RiderScriptEditorData.instance.initializedOnce)
  34. {
  35. installations = RiderPathLocator.GetAllRiderPaths().OrderBy(a => a.BuildNumber).ToArray();
  36. // is likely outdated
  37. if (installations.Any() && installations.All(a => GetEditorRealPath(a.Path) != path))
  38. {
  39. if (RiderPathLocator.GetIsToolbox(path)) // is toolbox - update
  40. {
  41. var toolboxInstallations = installations.Where(a => a.IsToolbox).ToArray();
  42. if (toolboxInstallations.Any())
  43. {
  44. var newEditor = toolboxInstallations.Last().Path;
  45. CodeEditor.SetExternalScriptEditor(newEditor);
  46. path = newEditor;
  47. }
  48. else
  49. {
  50. var newEditor = installations.Last().Path;
  51. CodeEditor.SetExternalScriptEditor(newEditor);
  52. path = newEditor;
  53. }
  54. }
  55. else // is non toolbox - notify
  56. {
  57. var newEditorName = installations.Last().Presentation;
  58. Debug.LogWarning($"Consider updating External Editor in Unity to Rider {newEditorName}.");
  59. }
  60. }
  61. ShowWarningOnUnexpectedScriptEditor(path);
  62. RiderScriptEditorData.instance.initializedOnce = true;
  63. }
  64. if (!FileSystemUtil.EditorPathExists(path)) // previously used rider was removed
  65. {
  66. if (installations == null)
  67. installations = RiderPathLocator.GetAllRiderPaths().OrderBy(a => a.BuildNumber).ToArray();
  68. if (installations.Any())
  69. {
  70. var newEditor = installations.Last().Path;
  71. CodeEditor.SetExternalScriptEditor(newEditor);
  72. path = newEditor;
  73. }
  74. }
  75. RiderScriptEditorData.instance.Init();
  76. editor.CreateSolutionIfDoesntExist();
  77. if (RiderScriptEditorData.instance.shouldLoadEditorPlugin)
  78. {
  79. editor.m_Initiliazer.Initialize(path);
  80. }
  81. RiderFileSystemWatcher.InitWatcher(
  82. Directory.GetCurrentDirectory(), "*.*", (sender, args) =>
  83. {
  84. var extension = Path.GetExtension(args.Name);
  85. if (extension == ".sln" || extension == ".csproj")
  86. RiderScriptEditorData.instance.hasChanges = true;
  87. });
  88. RiderFileSystemWatcher.InitWatcher(
  89. Path.Combine(Directory.GetCurrentDirectory(), "Library"),
  90. "EditorOnlyScriptingUserSettings.json",
  91. (sender, args) => { RiderScriptEditorData.instance.hasChanges = true; });
  92. RiderFileSystemWatcher.InitWatcher(
  93. Path.Combine(Directory.GetCurrentDirectory(), "Packages"),
  94. "manifest.json", (sender, args) => { RiderScriptEditorData.instance.hasChanges = true; });
  95. // can't switch to non-deprecated api, because UnityEditor.Build.BuildPipelineInterfaces.processors is internal
  96. #pragma warning disable 618
  97. EditorUserBuildSettings.activeBuildTargetChanged += () =>
  98. #pragma warning restore 618
  99. {
  100. RiderScriptEditorData.instance.hasChanges = true;
  101. };
  102. }
  103. }
  104. catch (Exception e)
  105. {
  106. Debug.LogException(e);
  107. }
  108. }
  109. private static void ShowWarningOnUnexpectedScriptEditor(string path)
  110. {
  111. // Show warning, when Unity was started from Rider, but external editor is different https://github.com/JetBrains/resharper-unity/issues/1127
  112. try
  113. {
  114. var args = Environment.GetCommandLineArgs();
  115. var commandlineParser = new CommandLineParser(args);
  116. if (commandlineParser.Options.ContainsKey("-riderPath"))
  117. {
  118. var originRiderPath = commandlineParser.Options["-riderPath"];
  119. var originRealPath = GetEditorRealPath(originRiderPath);
  120. var originVersion = RiderPathLocator.GetBuildNumber(originRealPath);
  121. var version = RiderPathLocator.GetBuildNumber(path);
  122. if (originVersion != null && originVersion != version)
  123. {
  124. Debug.LogWarning("Unity was started by a version of Rider that is not the current default external editor. Advanced integration features cannot be enabled.");
  125. Debug.Log($"Unity was started by Rider {originVersion}, but external editor is set to: {path}");
  126. }
  127. }
  128. }
  129. catch (Exception e)
  130. {
  131. Debug.LogException(e);
  132. }
  133. }
  134. internal static string GetEditorRealPath(string path)
  135. {
  136. if (string.IsNullOrEmpty(path))
  137. {
  138. return path;
  139. }
  140. if (!FileSystemUtil.EditorPathExists(path))
  141. return path;
  142. if (SystemInfo.operatingSystemFamily != OperatingSystemFamily.Windows)
  143. {
  144. var realPath = FileSystemUtil.GetFinalPathName(path);
  145. // case of snap installation
  146. if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Linux)
  147. {
  148. if (new FileInfo(path).Name.ToLowerInvariant() == "rider" &&
  149. new FileInfo(realPath).Name.ToLowerInvariant() == "snap")
  150. {
  151. var snapInstallPath = "/snap/rider/current/bin/rider.sh";
  152. if (new FileInfo(snapInstallPath).Exists)
  153. return snapInstallPath;
  154. }
  155. }
  156. // in case of symlink
  157. return realPath;
  158. }
  159. return path;
  160. }
  161. public RiderScriptEditor(IDiscovery discovery, IGenerator projectGeneration)
  162. {
  163. m_Discoverability = discovery;
  164. m_ProjectGeneration = projectGeneration;
  165. }
  166. private static string[] defaultExtensions
  167. {
  168. get
  169. {
  170. var customExtensions = new[] {"json", "asmdef", "log", "xaml"};
  171. return EditorSettings.projectGenerationBuiltinExtensions.Concat(EditorSettings.projectGenerationUserExtensions)
  172. .Concat(customExtensions).Distinct().ToArray();
  173. }
  174. }
  175. private static string[] HandledExtensions
  176. {
  177. get
  178. {
  179. return HandledExtensionsString.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.TrimStart('.', '*'))
  180. .ToArray();
  181. }
  182. }
  183. private static string HandledExtensionsString
  184. {
  185. get { return EditorPrefs.GetString("Rider_UserExtensions", string.Join(";", defaultExtensions));}
  186. set { EditorPrefs.SetString("Rider_UserExtensions", value); }
  187. }
  188. private static bool SupportsExtension(string path)
  189. {
  190. var extension = Path.GetExtension(path);
  191. if (string.IsNullOrEmpty(extension))
  192. return false;
  193. // cs is a default extension, which should always be handled
  194. return extension == ".cs" || HandledExtensions.Contains(extension.TrimStart('.'));
  195. }
  196. public void OnGUI()
  197. {
  198. if (RiderScriptEditorData.instance.shouldLoadEditorPlugin)
  199. {
  200. HandledExtensionsString = EditorGUILayout.TextField(new GUIContent("Extensions handled: "), HandledExtensionsString);
  201. }
  202. EditorGUILayout.LabelField("Generate .csproj files for:");
  203. EditorGUI.indentLevel++;
  204. SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "");
  205. SettingsButton(ProjectGenerationFlag.Local, "Local packages", "");
  206. SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "");
  207. SettingsButton(ProjectGenerationFlag.Git, "Git packages", "");
  208. SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "");
  209. #if UNITY_2019_3_OR_NEWER
  210. SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "");
  211. #endif
  212. SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "");
  213. SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'");
  214. RegenerateProjectFiles();
  215. EditorGUI.indentLevel--;
  216. }
  217. void RegenerateProjectFiles()
  218. {
  219. var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] {}));
  220. rect.width = 252;
  221. if (GUI.Button(rect, "Regenerate project files"))
  222. {
  223. m_ProjectGeneration.Sync();
  224. }
  225. }
  226. void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip)
  227. {
  228. var prevValue = m_ProjectGeneration.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
  229. var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
  230. if (newValue != prevValue)
  231. {
  232. m_ProjectGeneration.AssemblyNameProvider.ToggleProjectGeneration(preference);
  233. }
  234. }
  235. public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles,
  236. string[] importedFiles)
  237. {
  238. m_ProjectGeneration.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles),
  239. importedFiles);
  240. }
  241. public void SyncAll()
  242. {
  243. AssetDatabase.Refresh();
  244. m_ProjectGeneration.SyncIfNeeded(new string[] { }, new string[] { });
  245. }
  246. public void Initialize(string editorInstallationPath) // is called each time ExternalEditor is changed
  247. {
  248. RiderScriptEditorData.instance.Invalidate(editorInstallationPath);
  249. m_ProjectGeneration.Sync(); // regenerate csproj and sln for new editor
  250. }
  251. public bool OpenProject(string path, int line, int column)
  252. {
  253. if (path != "" && !SupportsExtension(path)) // Assets - Open C# Project passes empty path here
  254. {
  255. return false;
  256. }
  257. if (path == "" && SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX)
  258. {
  259. // there is a bug in DllImplementation - use package implementation here instead https://github.cds.internal.unity3d.com/unity/com.unity.ide.rider/issues/21
  260. return OpenOSXApp(path, line, column);
  261. }
  262. if (!IsUnityScript(path))
  263. {
  264. m_ProjectGeneration.SyncIfNeeded(affectedFiles: new string[] { }, new string[] { });
  265. var fastOpenResult = EditorPluginInterop.OpenFileDllImplementation(path, line, column);
  266. if (fastOpenResult)
  267. return true;
  268. }
  269. if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX)
  270. {
  271. return OpenOSXApp(path, line, column);
  272. }
  273. var solution = GetSolutionFile(path); // TODO: If solution file doesn't exist resync.
  274. solution = solution == "" ? "" : $"\"{solution}\"";
  275. var process = new Process
  276. {
  277. StartInfo = new ProcessStartInfo
  278. {
  279. FileName = CodeEditor.CurrentEditorInstallation,
  280. Arguments = $"{solution} -l {line} \"{path}\"",
  281. UseShellExecute = true,
  282. }
  283. };
  284. process.Start();
  285. return true;
  286. }
  287. private bool OpenOSXApp(string path, int line, int column)
  288. {
  289. var solution = GetSolutionFile(path);
  290. solution = solution == "" ? "" : $"\"{solution}\"";
  291. var pathArguments = path == "" ? "" : $"-l {line} \"{path}\"";
  292. var process = new Process
  293. {
  294. StartInfo = new ProcessStartInfo
  295. {
  296. FileName = "open",
  297. Arguments = $"-n -j \"{CodeEditor.CurrentEditorInstallation}\" --args {solution} {pathArguments}",
  298. CreateNoWindow = true,
  299. UseShellExecute = true,
  300. }
  301. };
  302. process.Start();
  303. return true;
  304. }
  305. private string GetSolutionFile(string path)
  306. {
  307. if (IsUnityScript(path))
  308. {
  309. return Path.Combine(GetBaseUnityDeveloperFolder(), "Projects/CSharp/Unity.CSharpProjects.gen.sln");
  310. }
  311. var solutionFile = m_ProjectGeneration.SolutionFile();
  312. if (File.Exists(solutionFile))
  313. {
  314. return solutionFile;
  315. }
  316. return "";
  317. }
  318. static bool IsUnityScript(string path)
  319. {
  320. if (UnityEditor.Unsupported.IsDeveloperBuild())
  321. {
  322. var baseFolder = GetBaseUnityDeveloperFolder().Replace("\\", "/");
  323. var lowerPath = path.ToLowerInvariant().Replace("\\", "/");
  324. if (lowerPath.Contains((baseFolder + "/Runtime").ToLowerInvariant())
  325. || lowerPath.Contains((baseFolder + "/Editor").ToLowerInvariant()))
  326. {
  327. return true;
  328. }
  329. }
  330. return false;
  331. }
  332. static string GetBaseUnityDeveloperFolder()
  333. {
  334. return Directory.GetParent(EditorApplication.applicationPath).Parent.Parent.FullName;
  335. }
  336. public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
  337. {
  338. if (FileSystemUtil.EditorPathExists(editorPath) && IsRiderInstallation(editorPath))
  339. {
  340. var info = new RiderPathLocator.RiderInfo(editorPath, false);
  341. installation = new CodeEditor.Installation
  342. {
  343. Name = info.Presentation,
  344. Path = info.Path
  345. };
  346. return true;
  347. }
  348. installation = default;
  349. return false;
  350. }
  351. public static bool IsRiderInstallation(string path)
  352. {
  353. if (IsAssetImportWorkerProcess())
  354. return false;
  355. if (string.IsNullOrEmpty(path))
  356. {
  357. return false;
  358. }
  359. var fileInfo = new FileInfo(path);
  360. var filename = fileInfo.Name.ToLowerInvariant();
  361. return filename.StartsWith("rider", StringComparison.Ordinal);
  362. }
  363. private static bool IsAssetImportWorkerProcess()
  364. {
  365. #if UNITY_2020_2_OR_NEWER
  366. return UnityEditor.AssetDatabase.IsAssetImportWorkerProcess();
  367. #elif UNITY_2019_3_OR_NEWER
  368. return UnityEditor.Experimental.AssetDatabaseExperimental.IsAssetImportWorkerProcess();
  369. #else
  370. return false;
  371. #endif
  372. }
  373. public static string CurrentEditor // works fast, doesn't validate if executable really exists
  374. => EditorPrefs.GetString("kScriptsDefaultApp");
  375. public CodeEditor.Installation[] Installations => m_Discoverability.PathCallback();
  376. private void CreateSolutionIfDoesntExist()
  377. {
  378. if (!m_ProjectGeneration.HasSolutionBeenGenerated())
  379. {
  380. m_ProjectGeneration.Sync();
  381. }
  382. }
  383. }
  384. }