using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Collections; using System.IO; namespace UnityVersionControl { #region Data Models [Serializable] public enum FileStatus { Added, Modified, Deleted, Renamed, Copied, Untracked, Synced } [Serializable] public class FileChangeInfo { public string relativePath; public FileStatus status; public string changeDescription; public FileChangeInfo(string path, FileStatus status, string description = "") { this.relativePath = path ?? ""; this.status = status; this.changeDescription = description ?? ""; } } [Serializable] public class CommitInfo { public string hash; public string author; public string message; public DateTime date; } public struct PullResult { public int filesChanged; public bool hasConflicts; public int conflictCount; } public enum NotificationType { Info, Success, Warning, Error } [Serializable] public struct FileMetadata { public string size; public string type; public DateTime lastModified; } [Serializable] public struct NotificationInfo { public string title; public string message; public NotificationType type; public float timestamp; public float duration; } public struct ThumbnailRequest { public string assetPath; public int index; public int priority; } #endregion /// /// Production-ready Unity Version Control Window with performance optimizations /// and comprehensive error handling /// public class VersionControlWindow : EditorWindow { #region Constants private const int ITEMS_PER_FRAME = 3; private const float ITEM_HEIGHT = 80f; private const int CACHE_SIZE_LIMIT = 100; private const float THUMBNAIL_SIZE = 56f; private const float NOTIFICATION_DURATION = 3f; private const float ANIMATION_SPEED = 2f; #endregion #region Core Fields [SerializeField] private Vector2 scrollPosition; [SerializeField] private int selectedTab = 0; [SerializeField] private int selectedChangeTab = 0; [SerializeField] private string commitMessage = ""; [SerializeField] private bool isProcessing = false; private readonly string[] tabNames = { "Changes", "History", "Settings" }; private readonly string[] changeTabNames = { "Added", "Modified", "Deleted", "Renamed" }; private readonly List realChanges = new List(); private readonly List commitHistory = new List(); private FileSystemWatcher fileWatcher; #endregion #region Project Window Integration private static Dictionary _assetStatusCache; public static Dictionary assetStatusCache { get { if (_assetStatusCache == null) _assetStatusCache = new Dictionary(); return _assetStatusCache; } } private readonly Dictionary statusIconCache = new Dictionary(); #endregion #region Performance System private ThumbnailCache thumbnailCache; private readonly Queue thumbnailQueue = new Queue(); private bool isProcessingThumbnails = false; private int visibleStartIndex = 0; private int visibleEndIndex = 0; #endregion #region UI Enhancement Fields private readonly Dictionary pulseTimers = new Dictionary(); private readonly Queue notifications = new Queue(); private bool showQuickActions = true; private bool showMinimap = false; private Vector2 minimapScrollPosition; [SerializeField] private string globalSearchFilter = ""; private readonly List recentSearches = new List(); private bool showAdvancedFilters = false; private readonly HashSet statusFilters = new HashSet(); private FileChangeInfo contextMenuTarget; private Vector2 contextMenuPosition; private bool showingContextMenu = false; private float frameTime = 0f; private int frameCount = 0; private float averageFrameTime = 16.67f; private double lastUpdateTime; #endregion #region Visual Constants private static readonly Color backgroundColor = new Color(0.22f, 0.22f, 0.22f); private static readonly Color cardColor = new Color(0.28f, 0.28f, 0.28f); private static readonly Color accentColor = new Color(0.3f, 0.7f, 1f); private static readonly Color successColor = new Color(0.3f, 0.8f, 0.3f); private static readonly Color warningColor = new Color(1f, 0.8f, 0.2f); private static readonly Color errorColor = new Color(1f, 0.4f, 0.4f); private static readonly Color syncedColor = new Color(0.4f, 0.7f, 1f); #endregion #region Caches and Collections private readonly Dictionary textureCache = new Dictionary(); private readonly Dictionary metadataCache = new Dictionary(); private readonly Dictionary fileTypeColors = new Dictionary { [".cs"] = new Color(0.2f, 0.6f, 0.9f), [".unity"] = new Color(0.9f, 0.3f, 0.3f), [".prefab"] = new Color(0.3f, 0.7f, 0.9f), [".mat"] = new Color(0.8f, 0.4f, 0.8f), [".png"] = new Color(0.3f, 0.8f, 0.3f), [".jpg"] = new Color(0.3f, 0.8f, 0.3f), [".jpeg"] = new Color(0.3f, 0.8f, 0.3f), [".shader"] = new Color(0.9f, 0.7f, 0.2f), [".fbx"] = new Color(0.7f, 0.5f, 0.3f), [".wav"] = new Color(0.5f, 0.8f, 0.5f), [".mp4"] = new Color(0.8f, 0.6f, 0.2f), [".anim"] = new Color(0.6f, 0.4f, 0.8f), ["default"] = new Color(0.6f, 0.6f, 0.6f) }; private readonly HashSet trackedExtensions = new HashSet { ".cs", ".unity", ".prefab", ".mat", ".png", ".jpg", ".jpeg", ".asset", ".shader", ".anim", ".fbx", ".obj", ".blend", ".wav", ".mp3", ".ogg", ".mp4", ".mov", ".txt", ".json", ".xml" }; #endregion #region Unity Lifecycle [MenuItem("Window/Version Control/Version Control Window")] public static void ShowWindow() { var window = GetWindow(); window.titleContent = new GUIContent("Version Control"); window.minSize = new Vector2(450, 350); window.Initialize(); } private void OnEnable() { try { Initialize(); RegisterCallbacks(); StartFileSystemWatcher(); InitializeProjectWindowIcons(); InitializeAssetStatusCache(); InitializePerformanceFeatures(); lastUpdateTime = EditorApplication.timeSinceStartup; } catch (Exception ex) { Debug.LogError($"Failed to initialize VersionControlWindow: {ex.Message}"); } } private void OnDisable() { try { UnregisterCallbacks(); StopFileSystemWatcher(); CleanupProjectWindowIcons(); CleanupPerformanceFeatures(); CleanupCaches(); } catch (Exception ex) { Debug.LogError($"Error during VersionControlWindow cleanup: {ex.Message}"); } } private void OnGUI() { try { UpdateAnimations(); ProcessThumbnailQueueImmediate(); HandleKeyboardShortcuts(); HandleContextMenu(); DrawBackground(); DrawEnhancedHeader(); DrawEnhancedSearchBar(); DrawTabs(); EditorGUILayout.Space(8); switch (selectedTab) { case 0: DrawChangesTab(); break; case 1: DrawHistoryTab(); break; case 2: DrawSettingsTab(); break; } DrawMinimap(); DrawNotifications(); DrawContextMenu(); } catch (Exception ex) { Debug.LogError($"Error in VersionControl OnGUI: {ex.Message}"); DrawFallbackUI(); } } #endregion #region Initialization private void Initialize() { RefreshCommitHistory(); LoadCachedChanges(); } private void InitializePerformanceFeatures() { thumbnailCache = new ThumbnailCache(CACHE_SIZE_LIMIT); lastUpdateTime = EditorApplication.timeSinceStartup; } private void LoadCachedChanges() { try { if (assetStatusCache?.Count > 0) { var changedAssets = assetStatusCache.Where(kvp => kvp.Value != FileStatus.Synced).ToList(); foreach (var cachedAsset in changedAssets) { if (string.IsNullOrEmpty(cachedAsset.Key)) continue; if (!realChanges.Any(c => c.relativePath == cachedAsset.Key)) { var description = GetStatusDescription(cachedAsset.Value); realChanges.Add(new FileChangeInfo(cachedAsset.Key, cachedAsset.Value, description)); } } if (changedAssets.Count > 0) { Repaint(); } } } catch (Exception ex) { Debug.LogError($"Error loading cached changes: {ex.Message}"); } } private string GetStatusDescription(FileStatus status) { return status switch { FileStatus.Added => "Asset added", FileStatus.Modified => "Asset modified", FileStatus.Deleted => "Asset deleted", FileStatus.Renamed => "Asset renamed", _ => "Asset changed" }; } #endregion #region Enhanced Header Drawing private void DrawEnhancedHeader() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { DrawStatusIcon(); DrawMainTitle(); DrawQuickStats(); DrawQuickActionsToolbar(); } if (showAdvancedFilters) { EditorGUILayout.Space(4); DrawAdvancedFilters(); } }, 12); } private void DrawStatusIcon() { var iconRect = GUILayoutUtility.GetRect(32, 32); var pulse = GetPulseValue("status_icon"); var iconColor = Color.Lerp(accentColor, successColor, pulse); var iconTexture = CreateRoundedTexture(iconColor, 32, 6); if (iconTexture != null) { GUI.DrawTexture(iconRect, iconTexture); } var statusIcon = IsFileWatcherActive() ? "⚡" : "⏸"; var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel) { normal = { textColor = Color.white }, fontSize = 16, fontStyle = FontStyle.Bold }; GUI.Label(iconRect, statusIcon, iconStyle); if (iconRect.Contains(Event.current.mousePosition)) { var tooltip = IsFileWatcherActive() ? "Live tracking active - changes detected in real-time" : "File tracking offline - manual refresh required"; GUI.tooltip = tooltip; } } private void DrawMainTitle() { GUILayout.Space(12); using (new EditorGUILayout.VerticalScope()) { var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, normal = { textColor = Color.white } }; EditorGUILayout.LabelField("Unity Version Control", titleStyle); var subtitle = GetDynamicSubtitle(); var subtitleStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.8f, 0.8f, 0.8f) }, fontSize = 11 }; EditorGUILayout.LabelField(subtitle, subtitleStyle); } } private void DrawQuickStats() { GUILayout.FlexibleSpace(); using (new EditorGUILayout.VerticalScope()) { var perfColor = averageFrameTime < 20f ? successColor : (averageFrameTime < 33f ? warningColor : errorColor); var perfText = $"⚡ {averageFrameTime:F1}ms"; var perfStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = perfColor }, fontSize = 9, alignment = TextAnchor.MiddleRight }; EditorGUILayout.LabelField(perfText, perfStyle); var changeCount = GetFilteredChanges().Count; var countText = $"{changeCount} change{(changeCount != 1 ? "s" : "")}"; var countStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = changeCount > 0 ? warningColor : new Color(0.7f, 0.7f, 0.7f) }, fontSize = 10, alignment = TextAnchor.MiddleRight }; EditorGUILayout.LabelField(countText, countStyle); } } private void DrawQuickActionsToolbar() { if (!showQuickActions) return; GUILayout.Space(12); using (new EditorGUILayout.VerticalScope()) { using (new EditorGUILayout.HorizontalScope()) { DrawToolbarButton("🔄", "Refresh (F5)", RefreshAllChanges); DrawToolbarButton("⎇", "Branches (Ctrl+B)", TryOpenBranchManager); DrawToolbarButton("📋", "Diff (Ctrl+D)", ShowDiffForSelected); using (new EditorGUI.DisabledScope(isProcessing)) { DrawToolbarButton("⬇", "Pull", () => _ = PullChangesAsync()); } } using (new EditorGUILayout.HorizontalScope()) { DrawToolbarButton("🔍", "Search", SetFocusToSearch); DrawToolbarButton("⚙", "Settings", () => selectedTab = 2); DrawToolbarButton("📊", "Minimap", () => showMinimap = !showMinimap); DrawToolbarButton("🎯", "Focus", FocusOnChanges); } } } private void DrawToolbarButton(string icon, string tooltip, System.Action action) { var buttonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = accentColor }, fixedWidth = 24, fixedHeight = 24, fontSize = 12 }; if (GUILayout.Button(icon, buttonStyle)) action?.Invoke(); var lastRect = GUILayoutUtility.GetLastRect(); if (lastRect.Contains(Event.current.mousePosition)) { GUI.tooltip = tooltip; } } #endregion #region Enhanced Search and Filtering private void DrawEnhancedSearchBar() { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("🔍", GUILayout.Width(20)); GUI.SetNextControlName("GlobalSearch"); var newFilter = EditorGUILayout.TextField(globalSearchFilter); if (newFilter != globalSearchFilter) { globalSearchFilter = newFilter; if (!string.IsNullOrEmpty(newFilter) && !recentSearches.Contains(newFilter)) { recentSearches.Insert(0, newFilter); if (recentSearches.Count > 5) recentSearches.RemoveAt(5); } } if (GUILayout.Button("⚙", GUILayout.Width(25))) showAdvancedFilters = !showAdvancedFilters; if (GUILayout.Button("✕", GUILayout.Width(25))) globalSearchFilter = ""; } } private void DrawAdvancedFilters() { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("Filters:", GUILayout.Width(50)); EditorGUILayout.LabelField("Status:", GUILayout.Width(45)); foreach (FileStatus status in Enum.GetValues(typeof(FileStatus))) { if (status == FileStatus.Untracked || status == FileStatus.Synced) continue; var wasFiltered = statusFilters.Contains(status); var isFiltered = EditorGUILayout.Toggle(wasFiltered, GUILayout.Width(20)); if (isFiltered != wasFiltered) { if (isFiltered) statusFilters.Add(status); else statusFilters.Remove(status); } var statusStyle = new GUIStyle(EditorStyles.label) { fontSize = 9, normal = { textColor = GetStatusColor(status) } }; EditorGUILayout.LabelField(status.ToString().Substring(0, 1), statusStyle, GUILayout.Width(15)); } GUILayout.FlexibleSpace(); if (GUILayout.Button("Clear Filters", GUILayout.Width(80))) { statusFilters.Clear(); globalSearchFilter = ""; } } } private List GetFilteredChanges() { var changes = realChanges.Where(c => !string.IsNullOrEmpty(c.relativePath) && !c.relativePath.EndsWith(".meta")).ToList(); if (!string.IsNullOrEmpty(globalSearchFilter)) { changes = changes.Where(c => c.relativePath.ToLower().Contains(globalSearchFilter.ToLower()) || c.changeDescription.ToLower().Contains(globalSearchFilter.ToLower()) ).ToList(); } if (statusFilters.Count > 0) { changes = changes.Where(c => statusFilters.Contains(c.status)).ToList(); } return changes; } #endregion #region Changes Tab Drawing private void DrawChangesTab() { EditorGUILayout.Space(8); DrawChangesHeader(); DrawChangesTabs(); DrawSelectedChangesContentOptimized(); var totalChanges = GetFilteredChanges().Count; if (totalChanges > 0) DrawCommitSection(); } private void DrawChangesHeader() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { var totalChanges = GetFilteredChanges().Count; DrawSectionHeader($"Pending Changes ({totalChanges})", "Ready to commit"); GUILayout.FlexibleSpace(); if (totalChanges > 0) { var revertButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = errorColor }, fixedHeight = 24 }; if (GUILayout.Button("↶ REVERT ALL", revertButtonStyle, GUILayout.Width(100))) ShowRevertAllConfirmation(); } var refreshButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = accentColor }, fixedHeight = 24 }; if (GUILayout.Button("🔄 REFRESH", refreshButtonStyle, GUILayout.Width(80))) { RefreshAllChanges(); } } }, 8); } private void DrawChangesTabs() { EditorGUILayout.Space(4); using (new EditorGUILayout.HorizontalScope()) { for (int i = 0; i < changeTabNames.Length; i++) { var statusCount = GetStatusCount((FileStatus)i); var isSelected = selectedChangeTab == i; var buttonStyle = new GUIStyle(EditorStyles.toolbarButton); if (isSelected) { buttonStyle.normal.textColor = accentColor; buttonStyle.fontStyle = FontStyle.Bold; } else { buttonStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f); } buttonStyle.fontSize = 11; var tabLabel = statusCount > 0 ? $"{changeTabNames[i]} ({statusCount})" : changeTabNames[i]; if (GUILayout.Button(tabLabel, buttonStyle)) selectedChangeTab = i; } } } private void DrawSelectedChangesContentOptimized() { var selectedStatus = (FileStatus)selectedChangeTab; var filteredChanges = GetFilteredChanges().Where(c => c.status == selectedStatus).ToList(); if (filteredChanges.Count == 0) { DrawEmptyChangesState(selectedStatus); return; } CalculateVisibleRange(filteredChanges.Count); using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { scrollPosition = scrollView.scrollPosition; DrawCard(() => { // Virtual spacer for items above viewport if (visibleStartIndex > 0) { GUILayout.Space(visibleStartIndex * ITEM_HEIGHT); } // Draw only visible items for (int i = visibleStartIndex; i <= visibleEndIndex && i < filteredChanges.Count; i++) { var change = filteredChanges[i]; DrawFileEntryOptimized(change, GetStatusColor(selectedStatus), i); if (i < visibleEndIndex && i < filteredChanges.Count - 1) { DrawSeparator(1f, 4f); } } // Virtual spacer for items below viewport var remainingItems = filteredChanges.Count - visibleEndIndex - 1; if (remainingItems > 0) { GUILayout.Space(remainingItems * ITEM_HEIGHT); } }, 12); } } private void CalculateVisibleRange(int totalItems) { var rect = GUILayoutUtility.GetLastRect(); var scrollViewHeight = rect.height; if (scrollViewHeight <= 0) scrollViewHeight = 400; var visibleItems = Mathf.CeilToInt(scrollViewHeight / ITEM_HEIGHT) + 2; visibleStartIndex = Mathf.Max(0, Mathf.FloorToInt(scrollPosition.y / ITEM_HEIGHT) - 1); visibleEndIndex = Mathf.Min(totalItems - 1, visibleStartIndex + visibleItems); } private void DrawFileEntryOptimized(FileChangeInfo change, Color accentColor, int index) { var entryRect = EditorGUILayout.BeginVertical(GUILayout.Height(ITEM_HEIGHT)); // Handle context menu if (Event.current.type == EventType.ContextClick && entryRect.Contains(Event.current.mousePosition)) { contextMenuTarget = change; contextMenuPosition = Event.current.mousePosition; showingContextMenu = true; Event.current.Use(); } using (new EditorGUILayout.HorizontalScope()) { var thumbnail = GetOptimizedFileThumbnail(change.relativePath, index); DrawOptimizedFileThumbnail(change, thumbnail, THUMBNAIL_SIZE); GUILayout.Space(12); using (new EditorGUILayout.VerticalScope()) { DrawFileInfo(change, accentColor); } GUILayout.FlexibleSpace(); DrawFileActions(change, accentColor); } EditorGUILayout.EndVertical(); } private void DrawFileInfo(FileChangeInfo change, Color accentColor) { var fileName = Path.GetFileName(change.relativePath); var fileNameStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 13, normal = { textColor = Color.white } }; EditorGUILayout.LabelField(fileName, fileNameStyle); var displayPath = change.relativePath.StartsWith("Assets/") ? change.relativePath.Substring(7) : change.relativePath; var pathStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }, fontSize = 11 }; EditorGUILayout.LabelField(displayPath, pathStyle); EditorGUILayout.Space(2); using (new EditorGUILayout.HorizontalScope()) { var metadata = GetCachedFileMetadata(change.relativePath); DrawMetadataBadge(metadata.size, new Color(0.4f, 0.6f, 0.8f)); GUILayout.Space(4); DrawMetadataBadge(metadata.type, new Color(0.6f, 0.4f, 0.8f)); GUILayout.Space(4); DrawMetadataBadge(change.changeDescription, accentColor); } } private void DrawFileActions(FileChangeInfo change, Color accentColor) { using (new EditorGUILayout.VerticalScope(GUILayout.Width(80))) { if (change.status != FileStatus.Deleted) { var viewButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = accentColor }, fixedHeight = 22 }; if (GUILayout.Button("👁 VIEW", viewButtonStyle)) ViewFile(change); if (GUILayout.Button("📋 DIFF", viewButtonStyle)) TryShowDiff(change.relativePath); } var revertButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = errorColor }, fixedHeight = 22 }; if (GUILayout.Button("↶ REVERT", revertButtonStyle)) RevertFile(change); } } private void DrawEmptyChangesState(FileStatus status) { DrawCard(() => { var iconRect = GUILayoutUtility.GetRect(48, 48); var emptyTexture = CreateRoundedTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 12); if (emptyTexture != null) { GUI.DrawTexture(iconRect, emptyTexture); } EditorGUILayout.Space(8); var titleStyle = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, normal = { textColor = new Color(0.7f, 0.7f, 0.7f) } }; EditorGUILayout.LabelField($"No {status} Files", titleStyle); var subtitleStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleCenter, normal = { textColor = new Color(0.6f, 0.6f, 0.6f) }, wordWrap = true }; var message = GetEmptyStateMessage(status); EditorGUILayout.LabelField(message, subtitleStyle); }, 32); } private string GetEmptyStateMessage(FileStatus status) { return status switch { FileStatus.Added => "No new files have been added to the project.", FileStatus.Modified => "No existing files have been modified.", FileStatus.Deleted => "No files have been deleted from the project.", FileStatus.Renamed => "No files have been renamed or moved.", _ => "No changes detected." }; } private int GetStatusCount(FileStatus status) { return GetFilteredChanges().Count(c => c.status == status); } private void DrawCommitSection() { EditorGUILayout.Space(8); DrawCard(() => { DrawSectionHeader("Commit Changes", "Create a new commit with selected files"); EditorGUILayout.Space(4); var messageStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true, fontSize = 12 }; EditorGUILayout.LabelField("Commit Message:", EditorStyles.boldLabel); commitMessage = EditorGUILayout.TextArea(commitMessage, messageStyle, GUILayout.Height(50)); EditorGUILayout.Space(8); var canCommit = !string.IsNullOrEmpty(commitMessage.Trim()) && !isProcessing && GetFilteredChanges().Count > 0; using (new EditorGUI.DisabledScope(!canCommit)) { var commitButtonStyle = new GUIStyle(GUI.skin.button) { normal = { textColor = Color.white }, fixedHeight = 32, fontSize = 13, fontStyle = FontStyle.Bold }; if (canCommit) { var commitBg = CreateRoundedTexture(successColor, 32, 6); if (commitBg != null) commitButtonStyle.normal.background = commitBg; } if (GUILayout.Button("🚀 COMMIT & PUSH", commitButtonStyle)) _ = CommitAndPushAsync(); } }, 12); } #endregion #region Optimized Thumbnail System private Texture2D GetOptimizedFileThumbnail(string assetPath, int index) { if (thumbnailCache != null && thumbnailCache.TryGet(assetPath, out var cachedThumbnail)) { return cachedThumbnail; } var request = new ThumbnailRequest { assetPath = assetPath, index = index, priority = CalculatePriority(index) }; if (!thumbnailQueue.Any(r => r.assetPath == assetPath)) { thumbnailQueue.Enqueue(request); } return null; } private void ProcessThumbnailQueueImmediate() { if (isProcessingThumbnails || thumbnailQueue.Count == 0) return; isProcessingThumbnails = true; var processed = 0; while (thumbnailQueue.Count > 0 && processed < ITEMS_PER_FRAME) { var request = thumbnailQueue.Dequeue(); try { var thumbnail = LoadThumbnailImmediate(request.assetPath); if (thumbnail != null && thumbnailCache != null) { thumbnailCache.Add(request.assetPath, thumbnail); Repaint(); } } catch (Exception ex) { Debug.LogWarning($"Failed to load thumbnail for {request.assetPath}: {ex.Message}"); } processed++; } isProcessingThumbnails = false; } private Texture2D LoadThumbnailImmediate(string assetPath) { try { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset == null) return null; var extension = Path.GetExtension(assetPath).ToLower(); if (IsVisualAsset(extension)) { var preview = AssetPreview.GetAssetPreview(asset); if (preview != null) return preview; if (asset is Texture2D texture) return texture; if (asset is Sprite sprite) return sprite.texture; } return AssetPreview.GetMiniThumbnail(asset); } catch (Exception) { return null; } } private void DrawOptimizedFileThumbnail(FileChangeInfo change, Texture2D thumbnail, float size) { var thumbnailRect = GUILayoutUtility.GetRect(size, size); var bgTexture = CreateRoundedTexture(GetFileTypeColor(change.relativePath), (int)size, 8); if (bgTexture != null) { GUI.DrawTexture(thumbnailRect, bgTexture); } if (thumbnail != null) { var borderRect = new Rect(thumbnailRect.x + 2, thumbnailRect.y + 2, thumbnailRect.width - 4, thumbnailRect.height - 4); EditorGUI.DrawRect(borderRect, new Color(0.2f, 0.2f, 0.2f, 0.8f)); var imageRect = new Rect(thumbnailRect.x + 3, thumbnailRect.y + 3, thumbnailRect.width - 6, thumbnailRect.height - 6); GUI.DrawTexture(imageRect, thumbnail, ScaleMode.ScaleToFit); } else { var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel) { normal = { textColor = Color.white }, fontSize = 18, fontStyle = FontStyle.Bold }; GUI.Label(thumbnailRect, GetFileTypeIcon(change.relativePath), iconStyle); var loadingStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel) { normal = { textColor = new Color(1f, 1f, 1f, 0.5f) }, fontSize = 8 }; var loadingRect = new Rect(thumbnailRect.x, thumbnailRect.y + thumbnailRect.height - 12, thumbnailRect.width, 12); GUI.Label(loadingRect, "⏳", loadingStyle); } DrawStatusIcon(change, thumbnailRect); } private void DrawStatusIcon(FileChangeInfo change, Rect thumbnailRect) { var statusSize = 16; var statusRect = new Rect( thumbnailRect.x + thumbnailRect.width - statusSize - 2, thumbnailRect.y + 2, statusSize, statusSize ); if (statusIconCache.TryGetValue(change.status, out var statusIcon) && statusIcon != null) { GUI.DrawTexture(statusRect, statusIcon, ScaleMode.ScaleToFit); } else { var statusColor = GetStatusColor(change.status); var statusTexture = CreateRoundedTexture(statusColor, statusSize, statusSize / 2); if (statusTexture != null) { GUI.DrawTexture(statusRect, statusTexture); } } } private int CalculatePriority(int index) { var distanceFromVisible = Mathf.Min( Mathf.Abs(index - visibleStartIndex), Mathf.Abs(index - visibleEndIndex) ); return 100 - distanceFromVisible; } private bool IsVisualAsset(string extension) { return extension switch { ".png" or ".jpg" or ".jpeg" or ".tga" or ".bmp" or ".gif" or ".psd" or ".tiff" => true, ".mat" or ".prefab" or ".fbx" or ".obj" or ".blend" => true, _ => false }; } #endregion #region UI Enhancement Methods private void UpdateAnimations() { var currentTime = EditorApplication.timeSinceStartup; var deltaTime = (float)(currentTime - lastUpdateTime); lastUpdateTime = currentTime; // Update pulse timers var keysToUpdate = pulseTimers.Keys.ToList(); foreach (var key in keysToUpdate) { pulseTimers[key] += deltaTime; if (pulseTimers[key] > ANIMATION_SPEED) pulseTimers[key] = 0f; } // Update performance monitoring frameTime = deltaTime * 1000f; frameCount++; if (frameCount % 10 == 0) { averageFrameTime = Mathf.Lerp(averageFrameTime, frameTime, 0.1f); } } private float GetPulseValue(string key) { if (!pulseTimers.ContainsKey(key)) pulseTimers[key] = 0f; var time = pulseTimers[key]; return (Mathf.Sin(time * Mathf.PI) + 1f) * 0.5f; } private void HandleKeyboardShortcuts() { if (Event.current.type != EventType.KeyDown) return; switch (Event.current.keyCode) { case KeyCode.F5: RefreshAllChanges(); Event.current.Use(); break; case KeyCode.B when Event.current.control: TryOpenBranchManager(); Event.current.Use(); break; case KeyCode.D when Event.current.control: ShowDiffForSelected(); Event.current.Use(); break; case KeyCode.F when Event.current.control: SetFocusToSearch(); Event.current.Use(); break; } } private void HandleContextMenu() { if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && showingContextMenu) { showingContextMenu = false; contextMenuTarget = null; Event.current.Use(); } } private string GetDynamicSubtitle() { var changeCount = GetFilteredChanges().Count; if (isProcessing) return "Processing..."; if (changeCount == 0) return "Working directory clean • Ready for new changes"; var addedCount = realChanges.Count(c => c.status == FileStatus.Added); var modifiedCount = realChanges.Count(c => c.status == FileStatus.Modified); var deletedCount = realChanges.Count(c => c.status == FileStatus.Deleted); var parts = new List(); if (addedCount > 0) parts.Add($"{addedCount} added"); if (modifiedCount > 0) parts.Add($"{modifiedCount} modified"); if (deletedCount > 0) parts.Add($"{deletedCount} deleted"); return string.Join(" • ", parts) + " • Ready to commit"; } private void SetFocusToSearch() { GUI.FocusControl("GlobalSearch"); EditorGUIUtility.editingTextField = true; } private void FocusOnChanges() { selectedTab = 0; selectedChangeTab = 1; Repaint(); } private void ShowDiffForSelected() { var filteredChanges = GetFilteredChanges(); if (filteredChanges.Count > 0) { TryShowDiff(filteredChanges[0].relativePath); } else { ShowNotification("No files selected for diff", NotificationType.Warning); } } private void TryOpenBranchManager() { try { // Check if BranchAndStashManager exists using reflection var type = System.Type.GetType("UnityVersionControl.BranchAndStashManager"); if (type != null) { var method = type.GetMethod("ShowWindow", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); method?.Invoke(null, null); } else { ShowNotification("Branch Manager not available", NotificationType.Warning); } } catch (Exception ex) { Debug.LogWarning($"Could not open Branch Manager: {ex.Message}"); ShowNotification("Branch Manager not available", NotificationType.Warning); } } private void TryShowDiff(string filePath) { try { // Check if FileDiffViewer exists using reflection var type = System.Type.GetType("UnityVersionControl.FileDiffViewer"); if (type != null) { var method = type.GetMethod("ShowDiff", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); method?.Invoke(null, new object[] { filePath }); } else { ShowNotification("Diff Viewer not available", NotificationType.Warning); } } catch (Exception ex) { Debug.LogWarning($"Could not open Diff Viewer: {ex.Message}"); ShowNotification("Diff Viewer not available", NotificationType.Warning); } } private void ShowNotification(string message, NotificationType type = NotificationType.Info, string title = "", float duration = NOTIFICATION_DURATION) { notifications.Enqueue(new NotificationInfo { title = string.IsNullOrEmpty(title) ? type.ToString() : title, message = message, type = type, timestamp = Time.realtimeSinceStartup, duration = duration }); } #endregion #region Notification and Minimap Drawing private void DrawNotifications() { if (notifications.Count == 0) return; var notificationY = 50f; var notificationsToRemove = new List(); foreach (var notification in notifications) { if (Time.realtimeSinceStartup - notification.timestamp > notification.duration) { notificationsToRemove.Add(notification); continue; } DrawNotification(notification, notificationY); notificationY += 60f; } // Remove expired notifications foreach (var expired in notificationsToRemove) { var tempQueue = new Queue(); while (notifications.Count > 0) { var item = notifications.Dequeue(); if (!item.Equals(expired)) tempQueue.Enqueue(item); } while (tempQueue.Count > 0) { notifications.Enqueue(tempQueue.Dequeue()); } } } private void DrawNotification(NotificationInfo notification, float y) { var notificationRect = new Rect(position.width - 320, y, 300, 50); var alpha = Mathf.Clamp01((notification.duration - (Time.realtimeSinceStartup - notification.timestamp)) / notification.duration); var bgColor = notification.type switch { NotificationType.Success => successColor, NotificationType.Warning => warningColor, NotificationType.Error => errorColor, _ => accentColor }; bgColor.a = alpha * 0.9f; EditorGUI.DrawRect(notificationRect, bgColor); using (new GUILayout.AreaScope(notificationRect)) { GUILayout.Space(8); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(8); var icon = notification.type switch { NotificationType.Success => "✓", NotificationType.Warning => "⚠", NotificationType.Error => "✕", _ => "ℹ" }; EditorGUILayout.LabelField(icon, GUILayout.Width(20)); using (new EditorGUILayout.VerticalScope()) { var titleStyle = new GUIStyle(EditorStyles.boldLabel) { normal = { textColor = Color.white }, fontSize = 11 }; EditorGUILayout.LabelField(notification.title, titleStyle); var messageStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, 1f, 1f, 0.8f) }, fontSize = 9 }; EditorGUILayout.LabelField(notification.message, messageStyle); } } } } private void DrawMinimap() { if (!showMinimap) return; var minimapWidth = 150f; var minimapRect = new Rect(position.width - minimapWidth - 10, 100, minimapWidth, position.height - 150); GUI.Box(minimapRect, "", GUI.skin.window); using (new GUILayout.AreaScope(minimapRect)) { EditorGUILayout.LabelField("Overview", EditorStyles.boldLabel); using (var scrollView = new EditorGUILayout.ScrollViewScope(minimapScrollPosition, GUILayout.Height(minimapRect.height - 40))) { minimapScrollPosition = scrollView.scrollPosition; var filteredChanges = GetFilteredChanges(); foreach (var change in filteredChanges.Take(20)) { var color = GetStatusColor(change.status); var fileName = Path.GetFileName(change.relativePath); var miniStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = color } }; if (GUILayout.Button(fileName, miniStyle, GUILayout.Height(12))) { ViewFile(change); } } } } } private void DrawContextMenu() { if (!showingContextMenu || contextMenuTarget == null) return; var menuWidth = 200f; var menuHeight = 120f; var menuRect = new Rect(contextMenuPosition.x, contextMenuPosition.y, menuWidth, menuHeight); if (menuRect.xMax > position.width) menuRect.x = position.width - menuWidth; if (menuRect.yMax > position.height) menuRect.y = position.height - menuHeight; GUI.Box(menuRect, "", GUI.skin.window); using (new GUILayout.AreaScope(menuRect)) { GUILayout.Space(8); if (GUILayout.Button("📁 Show in Explorer")) { ShowFileInExplorer(contextMenuTarget.relativePath); CloseContextMenu(); } if (GUILayout.Button("👁 View Diff")) { TryShowDiff(contextMenuTarget.relativePath); CloseContextMenu(); } if (GUILayout.Button("↶ Revert Changes")) { RevertFile(contextMenuTarget); CloseContextMenu(); } if (GUILayout.Button("📋 Copy Path")) { EditorGUIUtility.systemCopyBuffer = contextMenuTarget.relativePath; ShowNotification("Path copied to clipboard", NotificationType.Info); CloseContextMenu(); } GUILayout.Space(4); if (GUILayout.Button("✕ Close")) { CloseContextMenu(); } } } private void ShowFileInExplorer(string assetPath) { try { var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath); if (File.Exists(fullPath)) { EditorUtility.RevealInFinder(fullPath); } else { ShowNotification("File not found on disk", NotificationType.Error); } } catch (Exception ex) { Debug.LogError($"Error showing file in explorer: {ex.Message}"); ShowNotification("Could not open file location", NotificationType.Error); } } private void CloseContextMenu() { showingContextMenu = false; contextMenuTarget = null; } #endregion #region Helper Methods and Drawing private void DrawBackground() { var rect = new Rect(0, 0, position.width, position.height); EditorGUI.DrawRect(rect, backgroundColor); } private void DrawCard(System.Action content, int padding = 12) { if (content == null) return; var rect = EditorGUILayout.BeginVertical(); EditorGUI.DrawRect(rect, cardColor); GUILayout.Space(padding); EditorGUILayout.BeginHorizontal(); GUILayout.Space(padding); EditorGUILayout.BeginVertical(); content.Invoke(); EditorGUILayout.EndVertical(); GUILayout.Space(padding); EditorGUILayout.EndHorizontal(); GUILayout.Space(padding); EditorGUILayout.EndVertical(); } private void DrawSectionHeader(string title, string subtitle = "") { EditorGUILayout.Space(4); var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, normal = { textColor = Color.white } }; EditorGUILayout.LabelField(title, headerStyle); if (!string.IsNullOrEmpty(subtitle)) { var subtitleStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.8f, 0.8f, 0.8f) }, fontSize = 11 }; EditorGUILayout.LabelField(subtitle, subtitleStyle); } EditorGUILayout.Space(4); } private void DrawSeparator(float thickness = 1f, float spacing = 8f) { EditorGUILayout.Space(spacing); var rect = EditorGUILayout.GetControlRect(false, thickness); EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f)); EditorGUILayout.Space(spacing); } private void DrawTabs() { EditorGUILayout.Space(8); using (new EditorGUILayout.HorizontalScope()) { for (int i = 0; i < tabNames.Length; i++) { var isSelected = selectedTab == i; var buttonStyle = new GUIStyle(GUI.skin.button); if (isSelected) { buttonStyle.normal.background = Texture2D.whiteTexture; buttonStyle.normal.textColor = backgroundColor; buttonStyle.fontStyle = FontStyle.Bold; } else { buttonStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f); } buttonStyle.fixedHeight = 36; if (GUILayout.Button(tabNames[i].ToUpper(), buttonStyle)) selectedTab = i; } } } private void DrawMetadataBadge(string text, Color color) { if (string.IsNullOrEmpty(text)) return; var badgeStyle = new GUIStyle(GUI.skin.box) { normal = { textColor = Color.white }, fontSize = 9, padding = new RectOffset(6, 6, 2, 2), margin = new RectOffset(0, 0, 0, 0) }; var badgeTexture = CreateRoundedTexture(color, 16, 4); if (badgeTexture != null) badgeStyle.normal.background = badgeTexture; GUILayout.Label(text.ToUpper(), badgeStyle); } private Texture2D CreateRoundedTexture(Color color, int size = 64, int cornerRadius = 8) { var key = $"{color}_{size}_{cornerRadius}"; if (textureCache.TryGetValue(key, out var cachedTexture) && cachedTexture != null) return cachedTexture; try { var texture = new Texture2D(size, size); var pixels = new Color[size * size]; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float distanceToCorner = float.MaxValue; if (x < cornerRadius && y < cornerRadius) distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, cornerRadius)); else if (x >= size - cornerRadius && y < cornerRadius) distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, cornerRadius)); else if (x < cornerRadius && y >= size - cornerRadius) distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, size - cornerRadius - 1)); else if (x >= size - cornerRadius && y >= size - cornerRadius) distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, size - cornerRadius - 1)); pixels[y * size + x] = (distanceToCorner <= cornerRadius || (x >= cornerRadius && x < size - cornerRadius) || (y >= cornerRadius && y < size - cornerRadius)) ? color : Color.clear; } } texture.SetPixels(pixels); texture.Apply(); textureCache[key] = texture; return texture; } catch (Exception ex) { Debug.LogWarning($"Failed to create rounded texture: {ex.Message}"); return null; } } private Color GetFileTypeColor(string filePath) { if (string.IsNullOrEmpty(filePath)) return fileTypeColors["default"]; var extension = Path.GetExtension(filePath).ToLower(); return fileTypeColors.TryGetValue(extension, out var color) ? color : fileTypeColors["default"]; } private string GetFileTypeIcon(string filePath) { if (string.IsNullOrEmpty(filePath)) return "FILE"; var extension = Path.GetExtension(filePath).ToLower(); return extension switch { ".cs" => "C#", ".unity" => "SC", ".prefab" => "PF", ".mat" => "MT", ".png" or ".jpg" or ".jpeg" => "IMG", ".shader" => "SH", ".fbx" or ".obj" or ".blend" => "3D", ".wav" or ".mp3" or ".ogg" => "AUD", ".mp4" or ".mov" or ".avi" => "VID", ".anim" or ".controller" => "ANI", ".txt" or ".json" or ".xml" => "TXT", _ => "FILE" }; } private Color GetStatusColor(FileStatus status) { return status switch { FileStatus.Added => successColor, FileStatus.Modified => warningColor, FileStatus.Deleted => errorColor, FileStatus.Renamed => accentColor, FileStatus.Synced => syncedColor, _ => Color.gray }; } private FileMetadata GetCachedFileMetadata(string assetPath) { if (metadataCache.TryGetValue(assetPath, out var cached)) { return cached; } var metadata = new FileMetadata { size = GetFileSize(assetPath), type = GetFileTypeDisplay(assetPath), lastModified = GetFileLastModified(assetPath) }; metadataCache[assetPath] = metadata; return metadata; } private string GetFileSize(string assetPath) { try { if (string.IsNullOrEmpty(assetPath)) return "?"; var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath); if (File.Exists(fullPath)) { var fileInfo = new FileInfo(fullPath); var bytes = fileInfo.Length; if (bytes < 1024) return $"{bytes}B"; if (bytes < 1024 * 1024) return $"{bytes / 1024}KB"; return $"{bytes / (1024 * 1024)}MB"; } } catch (Exception ex) { Debug.LogWarning($"Failed to get file size for {assetPath}: {ex.Message}"); } return "?"; } private string GetFileTypeDisplay(string filePath) { if (string.IsNullOrEmpty(filePath)) return "FILE"; var extension = Path.GetExtension(filePath).ToUpper().TrimStart('.'); return string.IsNullOrEmpty(extension) ? "FILE" : extension; } private DateTime GetFileLastModified(string assetPath) { try { if (string.IsNullOrEmpty(assetPath)) return DateTime.MinValue; var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath); if (File.Exists(fullPath)) { return File.GetLastWriteTime(fullPath); } } catch (Exception ex) { Debug.LogWarning($"Failed to get last modified time for {assetPath}: {ex.Message}"); } return DateTime.MinValue; } private void DrawFallbackUI() { EditorGUILayout.LabelField("Version Control - Error State", EditorStyles.boldLabel); if (GUILayout.Button("Restart")) { Close(); ShowWindow(); } } #endregion #region File Operations and Actions private void ViewFile(FileChangeInfo change) { try { if (change?.relativePath == null) return; if (change.status == FileStatus.Added) { AssetDatabase.Refresh(); } var asset = AssetDatabase.LoadAssetAtPath(change.relativePath); if (asset != null) { EditorUtility.FocusProjectWindow(); EditorGUIUtility.PingObject(asset); } else { ShowNotification($"Asset not found: {Path.GetFileName(change.relativePath)}", NotificationType.Warning); } } catch (Exception ex) { Debug.LogError($"Error viewing file: {ex.Message}"); ShowNotification("Could not view file", NotificationType.Error); } } private void RevertFile(FileChangeInfo change) { try { if (change?.relativePath == null) return; realChanges.Remove(change); UpdateAssetStatus(change.relativePath, FileStatus.Synced); ShowNotification($"Reverted {Path.GetFileName(change.relativePath)}", NotificationType.Success); Repaint(); } catch (Exception ex) { Debug.LogError($"Error reverting file: {ex.Message}"); ShowNotification("Could not revert file", NotificationType.Error); } } private void ShowRevertAllConfirmation() { if (EditorUtility.DisplayDialog( "Revert All Changes", "Are you sure you want to revert all pending changes?\n\nThis action cannot be undone.", "Revert All", "Cancel")) { RevertAllChanges(); } } private void RevertAllChanges() { try { var count = realChanges.Count; foreach (var change in realChanges.ToList()) { UpdateAssetStatus(change.relativePath, FileStatus.Synced); } realChanges.Clear(); ShowNotification($"Reverted {count} changes", NotificationType.Success); Repaint(); } catch (Exception ex) { Debug.LogError($"Error reverting all changes: {ex.Message}"); ShowNotification("Could not revert all changes", NotificationType.Error); } } public void AddRealChange(string relativePath, FileStatus status, string description) { try { if (string.IsNullOrEmpty(relativePath)) return; realChanges.RemoveAll(c => c.relativePath == relativePath); var change = new FileChangeInfo(relativePath, status, description); realChanges.Insert(0, change); UpdateAssetStatus(relativePath, status); Repaint(); } catch (Exception ex) { Debug.LogError($"Error adding real change: {ex.Message}"); } } private void RefreshAllChanges() { try { AssetDatabase.Refresh(); LoadCachedChanges(); ShowNotification("Changes refreshed", NotificationType.Success); Repaint(); } catch (Exception ex) { Debug.LogError($"Error during manual refresh: {ex.Message}"); ShowNotification("Refresh failed", NotificationType.Error); } } #endregion #region Async Operations (Simplified) private async Task CommitAndPushAsync() { isProcessing = true; try { var changeCount = GetFilteredChanges().Count; ShowNotification($"Committing {changeCount} changes...", NotificationType.Info); await Task.Delay(1000); AddToCommitHistory(); foreach (var change in realChanges.ToList()) { UpdateAssetStatus(change.relativePath, FileStatus.Synced); } realChanges.Clear(); await Task.Delay(1000); commitMessage = ""; ShowNotification("Changes committed successfully!", NotificationType.Success); } catch (Exception e) { ShowNotification($"Commit failed: {e.Message}", NotificationType.Error); } finally { isProcessing = false; } } private async Task PullChangesAsync() { isProcessing = true; try { ShowNotification("Pulling changes from remote...", NotificationType.Info); await Task.Delay(1500); var pullResult = await SimulatePullOperation(); if (pullResult.hasConflicts) { var shouldContinue = EditorUtility.DisplayDialog("Pull Conflicts Detected", $"Found {pullResult.conflictCount} conflicts that need resolution.\n\nOpen conflict resolver to continue?", "Resolve Conflicts", "Cancel Pull"); if (shouldContinue) { TryOpenConflictResolver(); } else { ShowNotification("Pull cancelled", NotificationType.Warning); } } else if (pullResult.filesChanged > 0) { await CompletePullOperation(pullResult); ShowNotification($"Pulled {pullResult.filesChanged} changes", NotificationType.Success); } else { ShowNotification("Already up to date", NotificationType.Info); } } catch (Exception e) { ShowNotification($"Pull failed: {e.Message}", NotificationType.Error); } finally { isProcessing = false; } } private void TryOpenConflictResolver() { try { var type = System.Type.GetType("UnityVersionControl.VersionControlConflictResolver"); if (type != null) { var method = type.GetMethod("ShowWindow", new[] { typeof(System.Action) }); method?.Invoke(null, new object[] { (System.Action)(() => { ShowNotification("Conflicts resolved successfully", NotificationType.Success); })}); } else { ShowNotification("Conflict Resolver not available", NotificationType.Warning); } } catch (Exception ex) { Debug.LogWarning($"Could not open Conflict Resolver: {ex.Message}"); ShowNotification("Conflict Resolver not available", NotificationType.Warning); } } private async Task CompletePullOperation(PullResult pullResult) { await Task.Delay(500); // Simulate some incoming changes var sampleFiles = new[] { "Assets/Scripts/EnemyAI.cs", "Assets/Prefabs/Weapon.prefab", "Assets/Materials/GroundMaterial.mat" }; for (int i = 0; i < Math.Min(pullResult.filesChanged, sampleFiles.Length); i++) { var filePath = sampleFiles[i]; var changeType = (FileStatus)(i % 3); // Rotate through Added, Modified, Deleted AddRealChange(filePath, changeType, "Updated from remote"); } RefreshAllChanges(); Repaint(); } private async Task SimulatePullOperation() { await Task.Delay(1000); var random = new System.Random(); var scenario = random.Next(0, 3); return scenario switch { 0 => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 }, 1 => new PullResult { filesChanged = random.Next(1, 5), hasConflicts = false, conflictCount = 0 }, 2 => new PullResult { filesChanged = random.Next(3, 8), hasConflicts = true, conflictCount = random.Next(1, 3) }, _ => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 } }; } private void AddToCommitHistory() { var newCommit = new CommitInfo { hash = Guid.NewGuid().ToString(), author = "Current User", message = commitMessage, date = DateTime.Now }; commitHistory.Insert(0, newCommit); } #endregion #region Simplified Stubs for Missing Features private void DrawHistoryTab() { EditorGUILayout.Space(8); DrawCard(() => { DrawSectionHeader("Commit History", "Recent project commits"); if (commitHistory.Count == 0) { EditorGUILayout.LabelField("No commits yet", EditorStyles.centeredGreyMiniLabel); return; } using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { scrollPosition = scrollView.scrollPosition; foreach (var commit in commitHistory.Take(10)) DrawCommit(commit); } }, 8); } private void DrawCommit(CommitInfo commit) { if (commit == null) return; DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { var hashText = string.IsNullOrEmpty(commit.hash) ? "unknown" : commit.hash.Substring(0, Math.Min(8, commit.hash.Length)); DrawMetadataBadge(hashText, accentColor); GUILayout.Space(8); using (new EditorGUILayout.VerticalScope()) { var messageStyle = new GUIStyle(EditorStyles.boldLabel) { normal = { textColor = Color.white } }; EditorGUILayout.LabelField(commit.message ?? "No message", messageStyle); using (new EditorGUILayout.HorizontalScope()) { var metaStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }, fontSize = 11 }; EditorGUILayout.LabelField($"by {commit.author ?? "Unknown"}", metaStyle); GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(commit.date.ToString("MMM dd, HH:mm"), metaStyle); } } } }, 8); EditorGUILayout.Space(4); } private void DrawSettingsTab() { EditorGUILayout.Space(8); DrawCard(() => { DrawSectionHeader("Repository Configuration", "Set up your version control settings"); EditorGUILayout.Space(8); EditorGUILayout.TextField("Repository Path", ""); EditorGUILayout.TextField("User Name", ""); EditorGUILayout.TextField("User Email", ""); EditorGUILayout.Toggle("Auto-stage .meta files", true); EditorGUILayout.Space(16); using (new EditorGUILayout.HorizontalScope()) { var initButtonStyle = new GUIStyle(GUI.skin.button) { fixedHeight = 32, normal = { textColor = successColor } }; if (GUILayout.Button("⚡ INITIALIZE REPOSITORY", initButtonStyle)) ShowNotification("Repository initialized", NotificationType.Success); GUILayout.Space(8); var cloneButtonStyle = new GUIStyle(GUI.skin.button) { fixedHeight = 32, normal = { textColor = accentColor } }; if (GUILayout.Button("📥 CLONE REPOSITORY", cloneButtonStyle)) ShowNotification("Clone dialog would open here", NotificationType.Info); } }, 16); } private void RefreshCommitHistory() { commitHistory.Clear(); var sampleCommits = new[] { new CommitInfo { hash = "a1b2c3d4", author = "John Doe", message = "Added new player movement system", date = DateTime.Now.AddHours(-2) }, new CommitInfo { hash = "e5f6g7h8", author = "Jane Smith", message = "Fixed enemy AI pathfinding bug", date = DateTime.Now.AddHours(-5) }, new CommitInfo { hash = "i9j0k1l2", author = "Bob Johnson", message = "Updated UI design for main menu", date = DateTime.Now.AddDays(-1) } }; commitHistory.AddRange(sampleCommits); } #endregion #region File System Integration (Simplified) private void RegisterCallbacks() { try { EditorApplication.projectChanged += OnProjectChanged; EditorApplication.hierarchyChanged += OnHierarchyChanged; } catch (Exception ex) { Debug.LogWarning($"Could not register callbacks: {ex.Message}"); } } private void UnregisterCallbacks() { try { EditorApplication.projectChanged -= OnProjectChanged; EditorApplication.hierarchyChanged -= OnHierarchyChanged; } catch (Exception ex) { Debug.LogWarning($"Could not unregister callbacks: {ex.Message}"); } } private void StartFileSystemWatcher() { // Simplified - just initialize tracking InitializeAssetStatusCache(); } private void StopFileSystemWatcher() { try { if (fileWatcher != null) { fileWatcher.EnableRaisingEvents = false; fileWatcher.Dispose(); fileWatcher = null; } } catch (Exception ex) { Debug.LogWarning($"Error stopping file watcher: {ex.Message}"); } } private void InitializeProjectWindowIcons() { try { EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI; LoadStatusIcons(); } catch (Exception ex) { Debug.LogWarning($"Could not initialize project window icons: {ex.Message}"); } } private void CleanupProjectWindowIcons() { try { EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemGUI; } catch (Exception ex) { Debug.LogWarning($"Error cleaning up project window icons: {ex.Message}"); } } private void LoadStatusIcons() { // Try to load icons, but don't fail if they don't exist try { statusIconCache[FileStatus.Added] = Resources.Load("VersionControl/Icons/icon_added"); statusIconCache[FileStatus.Modified] = Resources.Load("VersionControl/Icons/icon_modified"); statusIconCache[FileStatus.Deleted] = Resources.Load("VersionControl/Icons/icon_deleted"); statusIconCache[FileStatus.Renamed] = Resources.Load("VersionControl/Icons/icon_renamed"); statusIconCache[FileStatus.Synced] = Resources.Load("VersionControl/Icons/icon_synced"); } catch (Exception ex) { Debug.LogWarning($"Could not load status icons: {ex.Message}"); } } private void OnProjectWindowItemGUI(string guid, Rect selectionRect) { try { var assetPath = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta")) return; if (!IsTrackedAsset(assetPath)) return; var status = GetAssetStatus(assetPath); if (status == FileStatus.Untracked) return; DrawProjectWindowStatusIcon(selectionRect, status, assetPath); } catch (Exception ex) { // Silently handle errors to avoid spamming console Debug.LogWarning($"Error in project window item GUI: {ex.Message}"); } } private void DrawProjectWindowStatusIcon(Rect itemRect, FileStatus status, string assetPath) { if (!statusIconCache.TryGetValue(status, out var icon) || icon == null) return; var iconSize = 16f; var padding = 2f; var iconRect = new Rect( itemRect.xMax - iconSize - padding, itemRect.y + padding, iconSize, iconSize ); GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit); if (iconRect.Contains(Event.current.mousePosition)) { var tooltip = GetStatusTooltip(status, assetPath); GUI.tooltip = tooltip; } } private void InitializeAssetStatusCache() { try { var allAssets = AssetDatabase.GetAllAssetPaths() .Where(path => IsTrackedAsset(path)) .ToArray(); foreach (var assetPath in allAssets) { if (!assetStatusCache.ContainsKey(assetPath)) { assetStatusCache[assetPath] = FileStatus.Synced; } } } catch (Exception ex) { Debug.LogWarning($"Error initializing asset status cache: {ex.Message}"); } } private FileStatus GetAssetStatus(string assetPath) { var change = realChanges.FirstOrDefault(c => c.relativePath == assetPath); if (change != null) return change.status; return assetStatusCache.TryGetValue(assetPath, out var status) ? status : FileStatus.Synced; } private void UpdateAssetStatus(string assetPath, FileStatus status) { try { assetStatusCache[assetPath] = status; EditorApplication.RepaintProjectWindow(); } catch (Exception ex) { Debug.LogWarning($"Error updating asset status: {ex.Message}"); } } private bool IsTrackedAsset(string assetPath) { if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta")) return false; var extension = Path.GetExtension(assetPath).ToLower(); return trackedExtensions.Contains(extension); } private string GetStatusTooltip(FileStatus status, string assetPath) { var fileName = Path.GetFileName(assetPath); return status switch { FileStatus.Added => $"Added: {fileName}", FileStatus.Modified => $"Modified: {fileName}", FileStatus.Deleted => $"Deleted: {fileName}", FileStatus.Renamed => $"Renamed: {fileName}", FileStatus.Synced => $"Synced: {fileName}", _ => fileName }; } private bool IsFileWatcherActive() { return fileWatcher?.EnableRaisingEvents ?? false; } private void OnProjectChanged() { EditorApplication.delayCall += RefreshAllChanges; } private void OnHierarchyChanged() { try { var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); if (!string.IsNullOrEmpty(activeScene.path)) { AddRealChange(activeScene.path, FileStatus.Modified, "Scene hierarchy changed"); } } catch (Exception ex) { Debug.LogWarning($"Error in hierarchy changed: {ex.Message}"); } } #endregion #region Performance Helper Classes private class ThumbnailCache { private readonly Dictionary cache = new Dictionary(); private readonly Queue accessOrder = new Queue(); private readonly int maxSize; public ThumbnailCache(int maxSize) { this.maxSize = maxSize; } public bool TryGet(string key, out Texture2D texture) { return cache.TryGetValue(key, out texture); } public void Add(string key, Texture2D texture) { if (cache.ContainsKey(key)) return; while (cache.Count >= maxSize && accessOrder.Count > 0) { var oldest = accessOrder.Dequeue(); if (cache.TryGetValue(oldest, out var oldTexture)) { cache.Remove(oldest); if (oldTexture != null) DestroyImmediate(oldTexture); } } cache[key] = texture; accessOrder.Enqueue(key); } public void Clear() { foreach (var texture in cache.Values) { if (texture != null) DestroyImmediate(texture); } cache.Clear(); accessOrder.Clear(); } } #endregion #region Cleanup private void CleanupPerformanceFeatures() { try { thumbnailCache?.Clear(); thumbnailQueue.Clear(); } catch (Exception ex) { Debug.LogWarning($"Error cleaning up performance features: {ex.Message}"); } } private void CleanupCaches() { try { foreach (var texture in textureCache.Values) { if (texture != null) DestroyImmediate(texture); } textureCache.Clear(); foreach (var texture in statusIconCache.Values) { if (texture != null) DestroyImmediate(texture); } statusIconCache.Clear(); metadataCache.Clear(); } catch (Exception ex) { Debug.LogWarning($"Error cleaning up caches: {ex.Message}"); } } #endregion } }