설명 없음
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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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.Collections.Generic;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Runtime.CompilerServices;
  11. using UnityEditor;
  12. using UnityEngine;
  13. using Unity.CodeEditor;
  14. [assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")]
  15. [assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")]
  16. [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
  17. namespace Microsoft.Unity.VisualStudio.Editor
  18. {
  19. [InitializeOnLoad]
  20. public class VisualStudioEditor : IExternalCodeEditor
  21. {
  22. CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations
  23. .Result
  24. .Values
  25. .Select(v => v.ToCodeEditorInstallation())
  26. .ToArray();
  27. private static readonly AsyncOperation<Dictionary<string, IVisualStudioInstallation>> _discoverInstallations;
  28. static VisualStudioEditor()
  29. {
  30. if (!UnityInstallation.IsMainUnityEditorProcess)
  31. return;
  32. Discovery.Initialize();
  33. CodeEditor.Register(new VisualStudioEditor());
  34. _discoverInstallations = AsyncOperation<Dictionary<string, IVisualStudioInstallation>>.Run(DiscoverInstallations);
  35. }
  36. #if UNITY_2019_4_OR_NEWER && !UNITY_2020
  37. [InitializeOnLoadMethod]
  38. static void LegacyVisualStudioCodePackageDisabler()
  39. {
  40. // disable legacy Visual Studio Code packages
  41. var editor = CodeEditor.Editor.GetCodeEditorForPath("code.cmd");
  42. if (editor == null)
  43. return;
  44. if (editor is VisualStudioEditor)
  45. return;
  46. // only disable the com.unity.ide.vscode package
  47. var assembly = editor.GetType().Assembly;
  48. var assemblyName = assembly.GetName().Name;
  49. if (assemblyName != "Unity.VSCode.Editor")
  50. return;
  51. CodeEditor.Unregister(editor);
  52. }
  53. #endif
  54. private static Dictionary<string, IVisualStudioInstallation> DiscoverInstallations()
  55. {
  56. try
  57. {
  58. return Discovery
  59. .GetVisualStudioInstallations()
  60. .ToDictionary(i => Path.GetFullPath(i.Path), i => i);
  61. }
  62. catch (Exception ex)
  63. {
  64. Debug.LogError($"Error detecting Visual Studio installations: {ex}");
  65. return new Dictionary<string, IVisualStudioInstallation>();
  66. }
  67. }
  68. internal static bool IsEnabled => CodeEditor.CurrentEditor is VisualStudioEditor && UnityInstallation.IsMainUnityEditorProcess;
  69. // this one seems legacy and not used anymore
  70. // keeping it for now given it is public, so we need a major bump to remove it
  71. public void CreateIfDoesntExist()
  72. {
  73. if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
  74. return;
  75. var generator = installation.ProjectGenerator;
  76. if (!generator.HasSolutionBeenGenerated())
  77. generator.Sync();
  78. }
  79. public void Initialize(string editorInstallationPath)
  80. {
  81. }
  82. internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool lookupDiscoveredInstallations, out IVisualStudioInstallation installation)
  83. {
  84. editorPath = Path.GetFullPath(editorPath);
  85. // lookup for well known installations
  86. if (lookupDiscoveredInstallations && _discoverInstallations.Result.TryGetValue(editorPath, out installation))
  87. return true;
  88. return Discovery.TryDiscoverInstallation(editorPath, out installation);
  89. }
  90. public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
  91. {
  92. var result = TryGetVisualStudioInstallationForPath(editorPath, lookupDiscoveredInstallations: false, out var vsi);
  93. installation = vsi?.ToCodeEditorInstallation() ?? default;
  94. return result;
  95. }
  96. public void OnGUI()
  97. {
  98. GUILayout.BeginHorizontal();
  99. GUILayout.FlexibleSpace();
  100. if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
  101. return;
  102. var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly);
  103. var style = new GUIStyle
  104. {
  105. richText = true,
  106. margin = new RectOffset(0, 4, 0, 0)
  107. };
  108. GUILayout.Label($"<size=10><color=grey>{package.displayName} v{package.version} enabled</color></size>", style);
  109. GUILayout.EndHorizontal();
  110. EditorGUILayout.LabelField("Generate .csproj files for:");
  111. EditorGUI.indentLevel++;
  112. SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", installation);
  113. SettingsButton(ProjectGenerationFlag.Local, "Local packages", "", installation);
  114. SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "", installation);
  115. SettingsButton(ProjectGenerationFlag.Git, "Git packages", "", installation);
  116. SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "", installation);
  117. SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "", installation);
  118. SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "", installation);
  119. SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'", installation);
  120. RegenerateProjectFiles(installation);
  121. EditorGUI.indentLevel--;
  122. }
  123. private static void RegenerateProjectFiles(IVisualStudioInstallation installation)
  124. {
  125. var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect());
  126. rect.width = 252;
  127. if (GUI.Button(rect, "Regenerate project files"))
  128. {
  129. installation.ProjectGenerator.Sync();
  130. }
  131. }
  132. private static void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip, IVisualStudioInstallation installation)
  133. {
  134. var generator = installation.ProjectGenerator;
  135. var prevValue = generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
  136. var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
  137. if (newValue != prevValue)
  138. generator.AssemblyNameProvider.ToggleProjectGeneration(preference);
  139. }
  140. public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles)
  141. {
  142. if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
  143. {
  144. installation.ProjectGenerator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
  145. }
  146. foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb"))
  147. {
  148. var pdbFile = FileUtility.GetAssetFullPath(file);
  149. // skip Unity packages like com.unity.ext.nunit
  150. if (pdbFile.IndexOf($"{Path.DirectorySeparatorChar}com.unity.", StringComparison.OrdinalIgnoreCase) > 0)
  151. continue;
  152. var asmFile = Path.ChangeExtension(pdbFile, ".dll");
  153. if (!File.Exists(asmFile) || !Image.IsAssembly(asmFile))
  154. continue;
  155. if (Symbols.IsPortableSymbolFile(pdbFile))
  156. continue;
  157. Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format.");
  158. }
  159. }
  160. public void SyncAll()
  161. {
  162. if (TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
  163. {
  164. installation.ProjectGenerator.Sync();
  165. }
  166. }
  167. private static bool IsSupportedPath(string path, IGenerator generator)
  168. {
  169. // Path is empty with "Open C# Project", as we only want to open the solution without specific files
  170. if (string.IsNullOrEmpty(path))
  171. return true;
  172. // cs, uxml, uss, shader, compute, cginc, hlsl, glslinc, template are part of Unity builtin extensions
  173. // txt, xml, fnt, cd are -often- par of Unity user extensions
  174. // asdmdef is mandatory included
  175. return generator.IsSupportedFile(path);
  176. }
  177. public bool OpenProject(string path, int line, int column)
  178. {
  179. var editorPath = CodeEditor.CurrentEditorInstallation;
  180. if (!Discovery.TryDiscoverInstallation(editorPath, out var installation)) {
  181. Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools.");
  182. return false;
  183. }
  184. var generator = installation.ProjectGenerator;
  185. if (!IsSupportedPath(path, generator))
  186. return false;
  187. if (!IsProjectGeneratedFor(path, generator, out var missingFlag))
  188. 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.");
  189. var solution = GetOrGenerateSolutionFile(generator);
  190. return installation.Open(path, line, column, solution);
  191. }
  192. private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag)
  193. {
  194. switch (flag)
  195. {
  196. case ProjectGenerationFlag.BuiltIn:
  197. return "Built-in packages";
  198. case ProjectGenerationFlag.Embedded:
  199. return "Embedded packages";
  200. case ProjectGenerationFlag.Git:
  201. return "Git packages";
  202. case ProjectGenerationFlag.Local:
  203. return "Local packages";
  204. case ProjectGenerationFlag.LocalTarBall:
  205. return "Local tarball";
  206. case ProjectGenerationFlag.PlayerAssemblies:
  207. return "Player projects";
  208. case ProjectGenerationFlag.Registry:
  209. return "Registry packages";
  210. case ProjectGenerationFlag.Unknown:
  211. return "Packages from unknown sources";
  212. default:
  213. return string.Empty;
  214. }
  215. }
  216. private static bool IsProjectGeneratedFor(string path, IGenerator generator, out ProjectGenerationFlag missingFlag)
  217. {
  218. missingFlag = ProjectGenerationFlag.None;
  219. // No need to check when opening the whole solution
  220. if (string.IsNullOrEmpty(path))
  221. return true;
  222. // We only want to check for cs scripts
  223. if (ProjectGeneration.ScriptingLanguageForFile(path) != ScriptingLanguage.CSharp)
  224. return true;
  225. // Even on windows, the package manager requires relative path + unix style separators for queries
  226. var basePath = generator.ProjectDirectory;
  227. var relativePath = path
  228. .NormalizeWindowsToUnix()
  229. .Replace(basePath, string.Empty)
  230. .Trim(FileUtility.UnixSeparator);
  231. var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(relativePath);
  232. if (packageInfo == null)
  233. return true;
  234. var source = packageInfo.source;
  235. if (!Enum.TryParse<ProjectGenerationFlag>(source.ToString(), out var flag))
  236. return true;
  237. if (generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag))
  238. return true;
  239. // Return false if we found a source not flagged for generation
  240. missingFlag = flag;
  241. return false;
  242. }
  243. private static string GetOrGenerateSolutionFile(IGenerator generator)
  244. {
  245. generator.Sync();
  246. return generator.SolutionFile();
  247. }
  248. }
  249. }