A game about forced loneliness, made by TACStudios
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}