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