| |
| |
| |
| |
| |
| 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; |
|
|
| |
| |
| 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) |
| { |
| |
| 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); |
|
|
| |
| 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) |
| { |
| |
| if (string.IsNullOrEmpty(path)) |
| return true; |
|
|
| |
| |
| |
| 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; |
|
|
| |
| if (string.IsNullOrEmpty(path)) |
| return true; |
|
|
| |
| if (ProjectGeneration.ScriptingLanguageForFile(path) != ScriptingLanguage.CSharp) |
| return true; |
|
|
| |
| 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; |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| 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; |
|
|
| |
| 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); |
|
|
| |
| 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(); |
| } |
| } |
| } |
|
|