123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Unity Technologies.
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- using System;
- using System.IO;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.Runtime.CompilerServices;
- using UnityEditor;
- using UnityEngine;
- using Unity.CodeEditor;
- using System.Threading;
- using System.Collections.Concurrent;
-
- [assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")]
- [assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")]
- [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
-
- namespace Microsoft.Unity.VisualStudio.Editor
- {
- [InitializeOnLoad]
- public class VisualStudioEditor : IExternalCodeEditor
- {
- internal static bool IsOSX => Application.platform == RuntimePlatform.OSXEditor;
- internal static bool IsWindows => !IsOSX && Path.DirectorySeparatorChar == FileUtility.WinSeparator && Environment.NewLine == "\r\n";
-
- CodeEditor.Installation[] IExternalCodeEditor.Installations => _discoverInstallations.Result
- .Select(i => i.ToCodeEditorInstallation())
- .ToArray();
-
- private static readonly AsyncOperation<IVisualStudioInstallation[]> _discoverInstallations;
-
- private readonly IGenerator _generator = new ProjectGeneration();
-
- static VisualStudioEditor()
- {
- if (!UnityInstallation.IsMainUnityEditorProcess)
- return;
-
- if (IsWindows)
- Discovery.FindVSWhere();
-
- CodeEditor.Register(new VisualStudioEditor());
-
- _discoverInstallations = AsyncOperation<IVisualStudioInstallation[]>.Run(DiscoverInstallations);
- }
-
- private static IVisualStudioInstallation[] DiscoverInstallations()
- {
- try
- {
- return Discovery
- .GetVisualStudioInstallations()
- .ToArray();
- }
- catch (Exception ex)
- {
- UnityEngine.Debug.LogError($"Error detecting Visual Studio installations: {ex}");
- return Array.Empty<IVisualStudioInstallation>();
- }
- }
-
- internal static bool IsEnabled => CodeEditor.CurrentEditor is VisualStudioEditor && UnityInstallation.IsMainUnityEditorProcess;
-
- // this one seems legacy and not used anymore
- // keeping it for now given it is public, so we need a major bump to remove it
- public void CreateIfDoesntExist()
- {
- if (!_generator.HasSolutionBeenGenerated())
- _generator.Sync();
- }
-
- public void Initialize(string editorInstallationPath)
- {
- }
-
- internal virtual bool TryGetVisualStudioInstallationForPath(string editorPath, bool searchInstallations, out IVisualStudioInstallation installation)
- {
- if (searchInstallations)
- {
- // lookup for well known installations
- foreach (var candidate in _discoverInstallations.Result)
- {
- if (!string.Equals(Path.GetFullPath(editorPath), Path.GetFullPath(candidate.Path), StringComparison.OrdinalIgnoreCase))
- continue;
-
- installation = candidate;
- return true;
- }
- }
-
- return Discovery.TryDiscoverInstallation(editorPath, out installation);
- }
-
- public virtual bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
- {
- var result = TryGetVisualStudioInstallationForPath(editorPath, searchInstallations: false, out var vsi);
- installation = vsi == null ? default : vsi.ToCodeEditorInstallation();
- return result;
- }
-
- public void OnGUI()
- {
- GUILayout.BeginHorizontal();
- GUILayout.FlexibleSpace();
-
- var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly);
-
- var style = new GUIStyle
- {
- richText = true,
- margin = new RectOffset(0, 4, 0, 0)
- };
-
- GUILayout.Label($"<size=10><color=grey>{package.displayName} v{package.version} enabled</color></size>", style);
- GUILayout.EndHorizontal();
-
- EditorGUILayout.LabelField("Generate .csproj files for:");
- EditorGUI.indentLevel++;
- SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "");
- SettingsButton(ProjectGenerationFlag.Local, "Local packages", "");
- SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "");
- SettingsButton(ProjectGenerationFlag.Git, "Git packages", "");
- SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "");
- SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "");
- SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "");
- SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'");
- RegenerateProjectFiles();
- EditorGUI.indentLevel--;
- }
-
- void RegenerateProjectFiles()
- {
- var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(new GUILayoutOption[] { }));
- rect.width = 252;
- if (GUI.Button(rect, "Regenerate project files"))
- {
- _generator.Sync();
- }
- }
-
- void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip)
- {
- var prevValue = _generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference);
- var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue);
- if (newValue != prevValue)
- {
- _generator.AssemblyNameProvider.ToggleProjectGeneration(preference);
- }
- }
-
- public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles)
- {
- _generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
-
- foreach (var file in importedFiles.Where(a => Path.GetExtension(a) == ".pdb"))
- {
- var pdbFile = FileUtility.GetAssetFullPath(file);
-
- // skip Unity packages like com.unity.ext.nunit
- if (pdbFile.IndexOf($"{Path.DirectorySeparatorChar}com.unity.", StringComparison.OrdinalIgnoreCase) > 0)
- continue;
-
- var asmFile = Path.ChangeExtension(pdbFile, ".dll");
- if (!File.Exists(asmFile) || !Image.IsAssembly(asmFile))
- continue;
-
- if (Symbols.IsPortableSymbolFile(pdbFile))
- continue;
-
- UnityEngine.Debug.LogWarning($"Unity is only able to load mdb or portable-pdb symbols. {file} is using a legacy pdb format.");
- }
- }
-
- public void SyncAll()
- {
- _generator.Sync();
- }
-
- bool IsSupportedPath(string path)
- {
- // Path is empty with "Open C# Project", as we only want to open the solution without specific files
- if (string.IsNullOrEmpty(path))
- return true;
-
- // cs, uxml, uss, shader, compute, cginc, hlsl, glslinc, template are part of Unity builtin extensions
- // txt, xml, fnt, cd are -often- par of Unity user extensions
- // asdmdef is mandatory included
- if (_generator.IsSupportedFile(path))
- return true;
-
- return false;
- }
-
- private static void CheckCurrentEditorInstallation()
- {
- var editorPath = CodeEditor.CurrentEditorInstallation;
- try
- {
- if (Discovery.TryDiscoverInstallation(editorPath, out _))
- return;
- }
- catch (IOException)
- {
- }
-
- UnityEngine.Debug.LogWarning($"Visual Studio executable {editorPath} is not found. Please change your settings in Edit > Preferences > External Tools.");
- }
-
- public bool OpenProject(string path, int line, int column)
- {
- CheckCurrentEditorInstallation();
-
- if (!IsSupportedPath(path))
- return false;
-
- if (!IsProjectGeneratedFor(path, out var missingFlag))
- 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.");
-
- if (IsOSX)
- return OpenOSXApp(path, line, column);
-
- if (IsWindows)
- return OpenWindowsApp(path, line);
-
- return false;
- }
-
- private static string GetProjectGenerationFlagDescription(ProjectGenerationFlag flag)
- {
- switch (flag)
- {
- case ProjectGenerationFlag.BuiltIn:
- return "Built-in packages";
- case ProjectGenerationFlag.Embedded:
- return "Embedded packages";
- case ProjectGenerationFlag.Git:
- return "Git packages";
- case ProjectGenerationFlag.Local:
- return "Local packages";
- case ProjectGenerationFlag.LocalTarBall:
- return "Local tarball";
- case ProjectGenerationFlag.PlayerAssemblies:
- return "Player projects";
- case ProjectGenerationFlag.Registry:
- return "Registry packages";
- case ProjectGenerationFlag.Unknown:
- return "Packages from unknown sources";
- default:
- return string.Empty;
- }
- }
-
- private bool IsProjectGeneratedFor(string path, out ProjectGenerationFlag missingFlag)
- {
- missingFlag = ProjectGenerationFlag.None;
-
- // No need to check when opening the whole solution
- if (string.IsNullOrEmpty(path))
- return true;
-
- // We only want to check for cs scripts
- if (ProjectGeneration.ScriptingLanguageForFile(path) != ScriptingLanguage.CSharp)
- return true;
-
- // Even on windows, the package manager requires relative path + unix style separators for queries
- var basePath = _generator.ProjectDirectory;
- var relativePath = FileUtility
- .NormalizeWindowsToUnix(path)
- .Replace(basePath, string.Empty)
- .Trim(FileUtility.UnixSeparator);
-
- var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(relativePath);
- if (packageInfo == null)
- return true;
-
- var source = packageInfo.source;
- if (!Enum.TryParse<ProjectGenerationFlag>(source.ToString(), out var flag))
- return true;
-
- if (_generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(flag))
- return true;
-
- // Return false if we found a source not flagged for generation
- missingFlag = flag;
- return false;
- }
-
- private enum COMIntegrationState
- {
- Running,
- DisplayProgressBar,
- ClearProgressBar,
- Exited
- }
-
- private bool OpenWindowsApp(string path, int line)
- {
- var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe");
-
- if (string.IsNullOrWhiteSpace(progpath))
- return false;
-
- string absolutePath = "";
- if (!string.IsNullOrWhiteSpace(path))
- {
- absolutePath = Path.GetFullPath(path);
- }
-
- // We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project
- // So process the fullpath to make it compatible with VS
- var solution = GetOrGenerateSolutionFile(path);
- if (!string.IsNullOrWhiteSpace(solution))
- {
- solution = $"\"{solution}\"";
- solution = solution.Replace("^", "^^");
- }
-
-
- var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}");
- psi.StandardOutputEncoding = System.Text.Encoding.Unicode;
- psi.StandardErrorEncoding = System.Text.Encoding.Unicode;
-
- // inter thread communication
- var messages = new BlockingCollection<COMIntegrationState>();
-
- var asyncStart = AsyncOperation<ProcessRunnerResult>.Run(
- () => ProcessRunner.StartAndWaitForExit(psi, onOutputReceived: data => OnOutputReceived(data, messages)),
- e => new ProcessRunnerResult {Success = false, Error = e.Message, Output = string.Empty},
- () => messages.Add(COMIntegrationState.Exited)
- );
-
- MonitorCOMIntegration(messages);
-
- var result = asyncStart.Result;
-
- if (!result.Success && !string.IsNullOrWhiteSpace(result.Error))
- Debug.LogError($"Error while starting Visual Studio: {result.Error}");
-
- return result.Success;
- }
-
- private static void MonitorCOMIntegration(BlockingCollection<COMIntegrationState> messages)
- {
- var displayingProgress = false;
- COMIntegrationState state;
-
- do
- {
- state = messages.Take();
- switch (state)
- {
- case COMIntegrationState.ClearProgressBar:
- EditorUtility.ClearProgressBar();
- displayingProgress = false;
- break;
- case COMIntegrationState.DisplayProgressBar:
- EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f);
- displayingProgress = true;
- break;
- }
- } while (state != COMIntegrationState.Exited);
-
- // Make sure the progress bar is properly cleared in case of COMIntegration failure
- if (displayingProgress)
- EditorUtility.ClearProgressBar();
- }
-
- private static readonly COMIntegrationState[] ProgressBarCommands = {COMIntegrationState.DisplayProgressBar, COMIntegrationState.ClearProgressBar};
- private static void OnOutputReceived(string data, BlockingCollection<COMIntegrationState> messages)
- {
- if (data == null)
- return;
-
- foreach (var cmd in ProgressBarCommands)
- {
- if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0)
- messages.Add(cmd);
- }
- }
-
- [DllImport("AppleEventIntegration")]
- static extern bool OpenVisualStudio(string appPath, string solutionPath, string filePath, int line);
-
- bool OpenOSXApp(string path, int line, int column)
- {
- string absolutePath = "";
- if (!string.IsNullOrWhiteSpace(path))
- {
- absolutePath = Path.GetFullPath(path);
- }
-
- var solution = GetOrGenerateSolutionFile(path);
- return OpenVisualStudio(CodeEditor.CurrentEditorInstallation, solution, absolutePath, line);
- }
-
- private string GetOrGenerateSolutionFile(string path)
- {
- _generator.Sync();
- return _generator.SolutionFile();
- }
- }
- }
|