VisualStudioEditor.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Unity Technologies.
  3. * Copyright (c) Microsoft Corporation. All rights reserved.
  4. * Licensed under the MIT License. See License.txt in the project root for license information.
  5. *--------------------------------------------------------------------------------------------*/
  6. using System;
  7. using System.Diagnostics;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Runtime.InteropServices;
  11. using System.Runtime.CompilerServices;
  12. using UnityEditor;
  13. using UnityEngine;
  14. using Unity.CodeEditor;
  15. [assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")]
  16. [assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")]
  17. [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
  18. namespace Microsoft.Unity.VisualStudio.Editor
  19. {
  20. [InitializeOnLoad]
  21. public class VisualStudioEditor : IExternalCodeEditor
  22. {
  23. internal static bool IsOSX => Application.platform == RuntimePlatform.OSXEditor;
  24. internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == FileUtility.WinSeparator && Environment.NewLine == "\r\n";
  25. CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations.Result
  26. .Select(i => i.ToCodeEditorInstallation())
  27. .ToArray();
  28. private static readonly AsyncOperation<IVisualStudioInstallation[]> _discoverInstallations;
  29. private readonly IGenerator _generator = new ProjectGeneration();
  30. static VisualStudioEditor()
  31. {
  32. if (!UnityInstallation.IsMainUnityEditorProcess)
  33. return;
  34. if (IsWindows)
  35. Discovery.FindVSWhere();
  36. CodeEditor.Register(new VisualStudioEditor());
  37. _discoverInstallations = AsyncOperation<IVisualStudioInstallation[]>.Run(DiscoverInstallations);
  38. }
  39. private static IVisualStudioInstallation[] DiscoverInstallations()
  40. {
  41. try
  42. {
  43. return Discovery
  44. .GetVisualStudioInstallations()
  45. .ToArray();
  46. }
  47. catch (Exception ex)
  48. {
  49. UnityEngine.Debug.LogError($"Error detecting Visual Studio installations: {ex}");
  50. return Array.Empty<IVisualStudioInstallation>();
  51. }
  52. }
  53. internal static bool IsEnabled => CodeEditor.CurrentEditor is VisualStudioEditor && UnityInstallation.IsMainUnityEditorProcess;
  54. public void CreateIfDoesntExist()
  55. {
  56. if (!_generator.HasSolutionBeenGenerated())
  57. _generator.Sync();
  58. }
  59. public void Initialize(string editorInstallationPath)
  60. {
  61. }
  62. internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool searchInstallations, out IVisualStudioInstallation installation)
  63. {
  64. if (searchInstallations)
  65. {
  66. // lookup for well known installations
  67. foreach (var candidate in _discoverInstallations.Result)
  68. {
  69. if (!string.Equals(Path.GetFullPath(editorPath), Path.GetFullPath(candidate.Path), StringComparison.OrdinalIgnoreCase))
  70. continue;
  71. installation = candidate;
  72. return true;
  73. }
  74. }
  75. return Discovery.TryDiscoverInstallation(editorPath, out installation);
  76. }
  77. public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
  78. {
  79. var result = TryGetVisualStudioInstallationForPath(editorPath, searchInstallations: false, out var vsi);
  80. installation = vsi == null ? default : vsi.ToCodeEditorInstallation();
  81. return result;
  82. }
  83. public void OnGUI()
  84. {
  85. GUILayout.BeginHorizontal();
  86. GUILayout.FlexibleSpace();
  87. var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly);
  88. var style = new GUIStyle
  89. {
  90. richText = true,
  91. margin = new RectOffset(0, 4, 0, 0)
  92. };
  93. GUILayout.Label($"<size=10><color=grey>{package.displayName} v{package.version} enabled</color></size>", style);
  94. GUILayout.EndHorizontal();
  95. EditorGUILayout.LabelField("Generate .csproj files for:");
  96. EditorGUI.indentLevel++;
  97. SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "");
  98. SettingsButton(ProjectGenerationFlag.Local, "Local packages", "");
  99. SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "");
  100. SettingsButton(ProjectGenerationFlag.Git, "Git packages", "");
  101. SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "");
  102. SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "");
  103. SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "");
  104. SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'");
  105. RegenerateProjectFiles();
  106. EditorGUI.indentLevel--;
  107. }
  108. void RegenerateProjectFiles()
  109. {
  110. var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] { }));
  111. rect.width = 252;
  112. if (GUI.Button(rect, "Regenerate project files"))
  113. {
  114. _generator.Sync();
  115. }
  116. }
  117. void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip)
  118. {
  119. var prevValue = _generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
  120. var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
  121. if (newValue != prevValue)
  122. {
  123. _generator.AssemblyNameProvider.ToggleProjectGeneration(preference);
  124. }
  125. }
  126. public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles)
  127. {
  128. _generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
  129. foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb"))
  130. {
  131. var pdbFile = FileUtility.GetAssetFullPath(file);
  132. // skip Unity packages like com.unity.ext.nunit
  133. if (pdbFile.IndexOf($"{Path.DirectorySeparatorChar}com.unity.", StringComparison.OrdinalIgnoreCase) > 0)
  134. continue;
  135. var asmFile = Path.ChangeExtension(pdbFile, ".dll");
  136. if (!File.Exists(asmFile) || !Image.IsAssembly(asmFile))
  137. continue;
  138. if (Symbols.IsPortableSymbolFile(pdbFile))
  139. continue;
  140. UnityEngine.Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format.");
  141. }
  142. }
  143. public void SyncAll()
  144. {
  145. AssetDatabase.Refresh();
  146. _generator.Sync();
  147. }
  148. bool IsSupportedPath(string path)
  149. {
  150. // Path is empty with "Open C# Project", as we only want to open the solution without specific files
  151. if (string.IsNullOrEmpty(path))
  152. return true;
  153. // cs, uxml, uss, shader, compute, cginc, hlsl, glslinc, template are part of Unity builtin extensions
  154. // txt, xml, fnt, cd are -often- par of Unity user extensions
  155. // asdmdef is mandatory included
  156. if (_generator.IsSupportedFile(path))
  157. return true;
  158. return false;
  159. }
  160. private static void CheckCurrentEditorInstallation()
  161. {
  162. var editorPath = CodeEditor.CurrentEditorInstallation;
  163. try
  164. {
  165. if (Discovery.TryDiscoverInstallation(editorPath, out _))
  166. return;
  167. }
  168. catch (IOException)
  169. {
  170. }
  171. UnityEngine.Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools.");
  172. }
  173. public bool OpenProject(string path, int line, int column)
  174. {
  175. CheckCurrentEditorInstallation();
  176. if (!IsSupportedPath(path))
  177. return false;
  178. if (!IsProjectGeneratedFor(path, out var missingFlag))
  179. UnityEngine.Debug.LogWarning($"You are trying to open {path} outside a generated project. This might cause problems with IntelliSense and debugging. To avoid this, you can change your .csproj preferences in Edit > Preferences > External Tools and enable {GetProjectGenerationFlagDescription(missingFlag)} generation.");
  180. if (IsOSX)
  181. return OpenOSXApp(path, line, column);
  182. if (IsWindows)
  183. return OpenWindowsApp(path, line);
  184. return false;
  185. }
  186. private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag)
  187. {
  188. switch (flag)
  189. {
  190. case ProjectGenerationFlag.BuiltIn:
  191. return "Built-in packages";
  192. case ProjectGenerationFlag.Embedded:
  193. return "Embedded packages";
  194. case ProjectGenerationFlag.Git:
  195. return "Git packages";
  196. case ProjectGenerationFlag.Local:
  197. return "Local packages";
  198. case ProjectGenerationFlag.LocalTarBall:
  199. return "Local tarball";
  200. case ProjectGenerationFlag.PlayerAssemblies:
  201. return "Player projects";
  202. case ProjectGenerationFlag.Registry:
  203. return "Registry packages";
  204. case ProjectGenerationFlag.Unknown:
  205. return "Packages from unknown sources";
  206. default:
  207. return string.Empty;
  208. }
  209. }
  210. private bool IsProjectGeneratedFor(string path, out ProjectGenerationFlag missingFlag)
  211. {
  212. missingFlag = ProjectGenerationFlag.None;
  213. // No need to check when opening the whole solution
  214. if (string.IsNullOrEmpty(path))
  215. return true;
  216. // We only want to check for cs scripts
  217. if (ProjectGeneration.ScriptingLanguageFor(path) != ScriptingLanguage.CSharp)
  218. return true;
  219. // Even on windows, the package manager requires relative path + unix style separators for queries
  220. var basePath = _generator.ProjectDirectory;
  221. var relativePath = FileUtility
  222. .NormalizeWindowsToUnix(path)
  223. .Replace(basePath, string.Empty)
  224. .Trim(FileUtility.UnixSeparator);
  225. var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(relativePath);
  226. if (packageInfo == null)
  227. return true;
  228. var source = packageInfo.source;
  229. if (!Enum.TryParse<ProjectGenerationFlag>(source.ToString(), out var flag))
  230. return true;
  231. if (_generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag))
  232. return true;
  233. // Return false if we found a source not flagged for generation
  234. missingFlag = flag;
  235. return false;
  236. }
  237. private bool OpenWindowsApp(string path, int line)
  238. {
  239. var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe");
  240. if (string.IsNullOrWhiteSpace(progpath))
  241. return false;
  242. string absolutePath = "";
  243. if (!string.IsNullOrWhiteSpace(path))
  244. {
  245. absolutePath = Path.GetFullPath(path);
  246. }
  247. // We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project
  248. // So process the fullpath to make it compatible with VS
  249. var solution = GetOrGenerateSolutionFile(path);
  250. if (!string.IsNullOrWhiteSpace(solution))
  251. {
  252. solution = $"\"{solution}\"";
  253. solution = solution.Replace("^", "^^");
  254. }
  255. var process = new Process
  256. {
  257. StartInfo = new ProcessStartInfo
  258. {
  259. FileName = progpath,
  260. Arguments = $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}",
  261. CreateNoWindow = true,
  262. UseShellExecute = false,
  263. RedirectStandardOutput = true,
  264. RedirectStandardError = true,
  265. }
  266. };
  267. var result = process.Start();
  268. while (!process.StandardOutput.EndOfStream)
  269. {
  270. var outputLine = process.StandardOutput.ReadLine();
  271. if (outputLine == "displayProgressBar")
  272. {
  273. EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f);
  274. }
  275. if (outputLine == "clearprogressbar")
  276. {
  277. EditorUtility.ClearProgressBar();
  278. }
  279. }
  280. var errorOutput = process.StandardError.ReadToEnd();
  281. if (!string.IsNullOrEmpty(errorOutput))
  282. {
  283. Console.WriteLine("Error: \n" + errorOutput);
  284. }
  285. process.WaitForExit();
  286. return result;
  287. }
  288. [DllImport("AppleEventIntegration")]
  289. static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line);
  290. bool OpenOSXApp(string path, int line, int column)
  291. {
  292. string absolutePath = "";
  293. if (!string.IsNullOrWhiteSpace(path))
  294. {
  295. absolutePath = Path.GetFullPath(path);
  296. }
  297. string solution = GetOrGenerateSolutionFile(path);
  298. return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line);
  299. }
  300. private string GetOrGenerateSolutionFile(string path)
  301. {
  302. var solution = GetSolutionFile(path);
  303. if (solution == "")
  304. {
  305. _generator.Sync();
  306. solution = GetSolutionFile(path);
  307. }
  308. return solution;
  309. }
  310. string GetSolutionFile(string path)
  311. {
  312. var solutionFile = _generator.SolutionFile();
  313. if (File.Exists(solutionFile))
  314. {
  315. return solutionFile;
  316. }
  317. return "";
  318. }
  319. }
  320. }