No Description
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.

VisualStudioEditor.cs 13KB

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