using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace UnityVersionControl { /// /// Advanced File Diff Viewer for Unity Assets /// Supports text diffs, visual comparisons, and property changes /// public class FileDiffViewer : EditorWindow { #region Fields private Vector2 scrollPosition; private int selectedViewMode = 0; private readonly string[] viewModeNames = { "Split View", "Unified Diff", "Visual Compare" }; private string currentFilePath = ""; private DiffData currentDiff; private Texture2D leftTexture; private Texture2D rightTexture; // UI state private bool showLineNumbers = true; private bool showWhitespace = false; private bool wordWrap = false; private int contextLines = 3; private string searchText = ""; // 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 addedColor = new Color(0.2f, 0.6f, 0.2f, 0.3f); private static readonly Color removedColor = new Color(0.6f, 0.2f, 0.2f, 0.3f); private static readonly Color modifiedColor = new Color(0.6f, 0.4f, 0.2f, 0.3f); private static readonly Color lineNumberColor = new Color(0.5f, 0.5f, 0.5f); private static readonly Color contextColor = new Color(0.8f, 0.8f, 0.8f); // Caches private readonly Dictionary textureCache = new Dictionary(); private readonly Dictionary diffCache = new Dictionary(); #endregion #region Unity Lifecycle [MenuItem("Window/Version Control/File Diff Viewer", false, 3)] public static void ShowWindow() { var window = GetWindow(); window.titleContent = new GUIContent("File Diff"); window.minSize = new Vector2(800, 600); } public static void ShowDiff(string filePath) { var window = GetWindow(); window.titleContent = new GUIContent($"Diff: {System.IO.Path.GetFileName(filePath)}"); window.minSize = new Vector2(800, 600); window.LoadFile(filePath); } private void OnEnable() { LoadTestData(); } private void OnDisable() { CleanupTextures(); } private void OnGUI() { DrawBackground(); DrawToolbar(); DrawDiffContent(); } #endregion #region File Loading public void LoadFile(string filePath) { currentFilePath = filePath; if (diffCache.TryGetValue(filePath, out var cachedDiff)) { currentDiff = cachedDiff; } else { currentDiff = GenerateDiff(filePath); diffCache[filePath] = currentDiff; } LoadAssetTextures(filePath); Repaint(); } private void LoadTestData() { // Load a test diff for demonstration LoadFile("Assets/Scripts/PlayerController.cs"); } private DiffData GenerateDiff(string filePath) { var extension = System.IO.Path.GetExtension(filePath).ToLower(); switch (extension) { case ".cs": case ".js": case ".shader": case ".hlsl": case ".cginc": return GenerateCodeDiff(filePath); case ".unity": case ".prefab": case ".asset": case ".mat": return GenerateAssetDiff(filePath); case ".png": case ".jpg": case ".jpeg": case ".tga": return GenerateImageDiff(filePath); default: return GenerateTextDiff(filePath); } } private DiffData GenerateCodeDiff(string filePath) { // Mock C# code diff var diff = new DiffData { filePath = filePath, diffType = DiffType.Code, leftContent = GetMockCodeContent(true), rightContent = GetMockCodeContent(false), lines = new List() }; diff.lines = GenerateDiffLines(diff.leftContent, diff.rightContent); return diff; } private DiffData GenerateAssetDiff(string filePath) { // Mock Unity asset property diff var diff = new DiffData { filePath = filePath, diffType = DiffType.Properties, leftContent = GetMockAssetContent(true), rightContent = GetMockAssetContent(false), propertyChanges = GeneratePropertyChanges() }; return diff; } private DiffData GenerateImageDiff(string filePath) { // Mock image diff var diff = new DiffData { filePath = filePath, diffType = DiffType.Image, leftImagePath = "Assets/Textures/PlayerTexture_Old.png", rightImagePath = "Assets/Textures/PlayerTexture_New.png" }; return diff; } private DiffData GenerateTextDiff(string filePath) { // Generic text diff var diff = new DiffData { filePath = filePath, diffType = DiffType.Text, leftContent = "Original content line 1\nOriginal content line 2\nShared line 3\nOriginal line 4", rightContent = "Modified content line 1\nNew line 2\nShared line 3\nModified line 4\nNew line 5" }; diff.lines = GenerateDiffLines(diff.leftContent, diff.rightContent); return diff; } private void LoadAssetTextures(string filePath) { if (currentDiff?.diffType == DiffType.Image) { leftTexture = LoadTextureFromPath(currentDiff.leftImagePath); rightTexture = LoadTextureFromPath(currentDiff.rightImagePath); } } private Texture2D LoadTextureFromPath(string path) { try { return AssetDatabase.LoadAssetAtPath(path); } catch { // Return a placeholder texture return CreateSolidColorTexture(Color.gray, 256, 256); } } #endregion #region Mock Data Generation private string GetMockCodeContent(bool isOld) { if (isOld) { return @"using UnityEngine; public class PlayerController : MonoBehaviour { public float speed = 5.0f; public float jumpForce = 8.0f; private Rigidbody rb; void Start() { rb = GetComponent(); } void Update() { float horizontal = Input.GetAxis(""Horizontal""); float vertical = Input.GetAxis(""Vertical""); Vector3 movement = new Vector3(horizontal, 0, vertical); transform.Translate(movement * speed * Time.deltaTime); if (Input.GetKeyDown(KeyCode.Space)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } }"; } else { return @"using UnityEngine; public class PlayerController : MonoBehaviour { [Header(""Movement Settings"")] public float speed = 7.0f; public float jumpForce = 10.0f; public float groundCheckDistance = 0.1f; private Rigidbody rb; private bool isGrounded; void Start() { rb = GetComponent(); } void Update() { CheckGrounded(); HandleMovement(); HandleJumping(); } void CheckGrounded() { isGrounded = Physics.Raycast(transform.position, Vector3.down, groundCheckDistance); } void HandleMovement() { float horizontal = Input.GetAxis(""Horizontal""); float vertical = Input.GetAxis(""Vertical""); Vector3 movement = new Vector3(horizontal, 0, vertical).normalized; rb.velocity = new Vector3(movement.x * speed, rb.velocity.y, movement.z * speed); } void HandleJumping() { if (Input.GetKeyDown(KeyCode.Space) && isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } }"; } } private string GetMockAssetContent(bool isOld) { if (isOld) { return @"Transform: position: {x: 0, y: 0, z: 0} rotation: {x: 0, y: 0, z: 0, w: 1} scale: {x: 1, y: 1, z: 1} MeshRenderer: material: PlayerMaterial receiveShadows: 1 BoxCollider: size: {x: 1, y: 1, z: 1} center: {x: 0, y: 0, z: 0}"; } else { return @"Transform: position: {x: 0, y: 0.5, z: 0} rotation: {x: 0, y: 0, z: 0, w: 1} scale: {x: 1.2, y: 1.2, z: 1.2} MeshRenderer: material: PlayerMaterial_Updated receiveShadows: 1 shadowCastingMode: 1 BoxCollider: size: {x: 1.1, y: 1.1, z: 1.1} center: {x: 0, y: 0, z: 0} isTrigger: 0 Rigidbody: mass: 1 drag: 0.5"; } } private List GeneratePropertyChanges() { return new List { new PropertyChange { propertyName = "Transform.position.y", oldValue = "0", newValue = "0.5", changeType = PropertyChangeType.Modified }, new PropertyChange { propertyName = "Transform.scale", oldValue = "{x: 1, y: 1, z: 1}", newValue = "{x: 1.2, y: 1.2, z: 1.2}", changeType = PropertyChangeType.Modified }, new PropertyChange { propertyName = "MeshRenderer.material", oldValue = "PlayerMaterial", newValue = "PlayerMaterial_Updated", changeType = PropertyChangeType.Modified }, new PropertyChange { propertyName = "MeshRenderer.shadowCastingMode", oldValue = "", newValue = "1", changeType = PropertyChangeType.Added }, new PropertyChange { propertyName = "BoxCollider.size", oldValue = "{x: 1, y: 1, z: 1}", newValue = "{x: 1.1, y: 1.1, z: 1.1}", changeType = PropertyChangeType.Modified }, new PropertyChange { propertyName = "Rigidbody", oldValue = "", newValue = "Component Added", changeType = PropertyChangeType.Added } }; } #endregion #region GUI Drawing private void DrawBackground() { var rect = new Rect(0, 0, position.width, position.height); EditorGUI.DrawRect(rect, backgroundColor); } private void DrawToolbar() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { // File info using (new EditorGUILayout.VerticalScope()) { var titleStyle = new GUIStyle(EditorStyles.boldLabel); titleStyle.fontSize = 14; titleStyle.normal.textColor = Color.white; EditorGUILayout.LabelField(System.IO.Path.GetFileName(currentFilePath), titleStyle); var pathStyle = new GUIStyle(EditorStyles.label); pathStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f); pathStyle.fontSize = 11; EditorGUILayout.LabelField(currentFilePath, pathStyle); } GUILayout.FlexibleSpace(); // View mode selector EditorGUILayout.LabelField("View:", GUILayout.Width(40)); selectedViewMode = EditorGUILayout.Popup(selectedViewMode, viewModeNames, GUILayout.Width(120)); GUILayout.Space(8); // Options showLineNumbers = EditorGUILayout.Toggle("Lines", showLineNumbers, GUILayout.Width(60)); showWhitespace = EditorGUILayout.Toggle("Space", showWhitespace, GUILayout.Width(60)); wordWrap = EditorGUILayout.Toggle("Wrap", wordWrap, GUILayout.Width(60)); } EditorGUILayout.Space(4); // Search bar using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("Search:", GUILayout.Width(50)); searchText = EditorGUILayout.TextField(searchText); if (GUILayout.Button("Clear", GUILayout.Width(50))) searchText = ""; } }, 12); } private void DrawDiffContent() { if (currentDiff == null) { DrawEmptyState(); return; } EditorGUILayout.Space(8); switch (selectedViewMode) { case 0: DrawSplitView(); break; case 1: DrawUnifiedView(); break; case 2: DrawVisualCompare(); break; } } private void DrawEmptyState() { DrawCard(() => { var iconRect = GUILayoutUtility.GetRect(48, 48); var emptyTexture = CreateSolidColorTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 48); GUI.DrawTexture(iconRect, emptyTexture); EditorGUILayout.Space(8); var titleStyle = new GUIStyle(EditorStyles.boldLabel); titleStyle.alignment = TextAnchor.MiddleCenter; titleStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f); EditorGUILayout.LabelField("No File Selected", titleStyle); var subtitleStyle = new GUIStyle(EditorStyles.label); subtitleStyle.alignment = TextAnchor.MiddleCenter; subtitleStyle.normal.textColor = new Color(0.6f, 0.6f, 0.6f); subtitleStyle.wordWrap = true; EditorGUILayout.LabelField("Select a modified file to view its diff.", subtitleStyle); }, 32); } private void DrawSplitView() { using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { scrollPosition = scrollView.scrollPosition; switch (currentDiff.diffType) { case DiffType.Code: case DiffType.Text: DrawCodeSplitView(); break; case DiffType.Properties: DrawPropertySplitView(); break; case DiffType.Image: DrawImageSplitView(); break; } } } private void DrawCodeSplitView() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { // Left panel header using (new EditorGUILayout.VerticalScope()) { var headerStyle = new GUIStyle(EditorStyles.boldLabel); headerStyle.normal.textColor = removedColor; headerStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField("BEFORE", headerStyle); DrawCodePanel(currentDiff.leftContent, true); } GUILayout.Space(8); // Right panel header using (new EditorGUILayout.VerticalScope()) { var headerStyle = new GUIStyle(EditorStyles.boldLabel); headerStyle.normal.textColor = addedColor; headerStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField("AFTER", headerStyle); DrawCodePanel(currentDiff.rightContent, false); } } }, 12); } private void DrawCodePanel(string content, bool isLeft) { var lines = content.Split('\n'); var codeStyle = new GUIStyle(EditorStyles.textArea); codeStyle.wordWrap = wordWrap; codeStyle.richText = true; codeStyle.font = EditorGUIUtility.Load("Consolas") as Font ?? GUI.skin.font; for (int i = 0; i < lines.Length; i++) { using (new EditorGUILayout.HorizontalScope()) { if (showLineNumbers) { var lineNumStyle = new GUIStyle(EditorStyles.label); lineNumStyle.normal.textColor = lineNumberColor; lineNumStyle.fontSize = 10; lineNumStyle.alignment = TextAnchor.MiddleRight; EditorGUILayout.LabelField((i + 1).ToString(), lineNumStyle, GUILayout.Width(30)); } var line = lines[i]; if (!string.IsNullOrEmpty(searchText) && line.Contains(searchText)) { line = line.Replace(searchText, $"{searchText}"); } var lineRect = EditorGUILayout.GetControlRect(); // Highlight changed lines var diffLine = GetDiffLineForIndex(i, isLeft); if (diffLine != null) { Color bgColor = diffLine.type switch { DiffLineType.Added => addedColor, DiffLineType.Removed => removedColor, DiffLineType.Modified => modifiedColor, _ => Color.clear }; if (bgColor != Color.clear) { EditorGUI.DrawRect(lineRect, bgColor); } } EditorGUI.LabelField(lineRect, line, codeStyle); } } } private void DrawPropertySplitView() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { // Properties comparison using (new EditorGUILayout.VerticalScope()) { var headerStyle = new GUIStyle(EditorStyles.boldLabel); headerStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField("PROPERTY CHANGES", headerStyle); EditorGUILayout.Space(8); foreach (var change in currentDiff.propertyChanges) { DrawPropertyChange(change); EditorGUILayout.Space(4); } } } }, 12); } private void DrawPropertyChange(PropertyChange change) { var changeRect = EditorGUILayout.BeginHorizontal(); Color bgColor = change.changeType switch { PropertyChangeType.Added => addedColor, PropertyChangeType.Removed => removedColor, PropertyChangeType.Modified => modifiedColor, _ => Color.clear }; if (bgColor != Color.clear) { EditorGUI.DrawRect(changeRect, bgColor); } GUILayout.Space(8); // Property name var nameStyle = new GUIStyle(EditorStyles.boldLabel); nameStyle.normal.textColor = Color.white; nameStyle.fontSize = 11; EditorGUILayout.LabelField(change.propertyName, nameStyle, GUILayout.Width(200)); // Change indicator var changeIcon = change.changeType switch { PropertyChangeType.Added => "+", PropertyChangeType.Removed => "-", PropertyChangeType.Modified => "⟳", _ => "?" }; var iconStyle = new GUIStyle(EditorStyles.label); iconStyle.normal.textColor = change.changeType switch { PropertyChangeType.Added => Color.green, PropertyChangeType.Removed => Color.red, PropertyChangeType.Modified => Color.yellow, _ => Color.white }; EditorGUILayout.LabelField(changeIcon, iconStyle, GUILayout.Width(20)); // Values using (new EditorGUILayout.VerticalScope()) { if (!string.IsNullOrEmpty(change.oldValue)) { var oldStyle = new GUIStyle(EditorStyles.label); oldStyle.normal.textColor = new Color(1f, 0.7f, 0.7f); oldStyle.fontSize = 10; EditorGUILayout.LabelField($"- {change.oldValue}", oldStyle); } if (!string.IsNullOrEmpty(change.newValue)) { var newStyle = new GUIStyle(EditorStyles.label); newStyle.normal.textColor = new Color(0.7f, 1f, 0.7f); newStyle.fontSize = 10; EditorGUILayout.LabelField($"+ {change.newValue}", newStyle); } } GUILayout.Space(8); EditorGUILayout.EndHorizontal(); } private void DrawImageSplitView() { DrawCard(() => { using (new EditorGUILayout.HorizontalScope()) { // Left image using (new EditorGUILayout.VerticalScope()) { var headerStyle = new GUIStyle(EditorStyles.boldLabel); headerStyle.normal.textColor = removedColor; headerStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField("BEFORE", headerStyle); DrawImagePanel(leftTexture, "Old Version"); } GUILayout.Space(8); // Right image using (new EditorGUILayout.VerticalScope()) { var headerStyle = new GUIStyle(EditorStyles.boldLabel); headerStyle.normal.textColor = addedColor; headerStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField("AFTER", headerStyle); DrawImagePanel(rightTexture, "New Version"); } } }, 12); } private void DrawImagePanel(Texture2D texture, string label) { if (texture != null) { var maxSize = 300f; var aspect = (float)texture.width / texture.height; var displayWidth = aspect > 1 ? maxSize : maxSize * aspect; var displayHeight = aspect > 1 ? maxSize / aspect : maxSize; var imageRect = GUILayoutUtility.GetRect(displayWidth, displayHeight); EditorGUI.DrawRect(imageRect, Color.black); GUI.DrawTexture(imageRect, texture, ScaleMode.ScaleToFit); // Image info var infoStyle = new GUIStyle(EditorStyles.label); infoStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f); infoStyle.fontSize = 10; infoStyle.alignment = TextAnchor.MiddleCenter; EditorGUILayout.LabelField($"{texture.width}x{texture.height}", infoStyle); } else { var placeholderRect = GUILayoutUtility.GetRect(300, 200); EditorGUI.DrawRect(placeholderRect, new Color(0.3f, 0.3f, 0.3f)); var labelStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel); labelStyle.normal.textColor = Color.white; GUI.Label(placeholderRect, label, labelStyle); } } private void DrawUnifiedView() { using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { scrollPosition = scrollView.scrollPosition; DrawCard(() => { if (currentDiff.lines != null && currentDiff.lines.Count > 0) { DrawUnifiedDiff(); } else { var noChangesStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel); noChangesStyle.normal.textColor = Color.white; EditorGUILayout.LabelField("No textual changes to display", noChangesStyle); } }, 12); } } private void DrawUnifiedDiff() { var codeStyle = new GUIStyle(EditorStyles.textArea); codeStyle.wordWrap = wordWrap; codeStyle.richText = true; codeStyle.font = EditorGUIUtility.Load("Consolas") as Font ?? GUI.skin.font; foreach (var line in currentDiff.lines) { using (new EditorGUILayout.HorizontalScope()) { if (showLineNumbers) { var lineNumStyle = new GUIStyle(EditorStyles.label); lineNumStyle.normal.textColor = lineNumberColor; lineNumStyle.fontSize = 10; lineNumStyle.alignment = TextAnchor.MiddleRight; var lineNumber = line.type == DiffLineType.Removed ? line.leftLineNumber : line.rightLineNumber; EditorGUILayout.LabelField(lineNumber > 0 ? lineNumber.ToString() : "", lineNumStyle, GUILayout.Width(30)); } var lineRect = EditorGUILayout.GetControlRect(); // Background color based on line type Color bgColor = line.type switch { DiffLineType.Added => addedColor, DiffLineType.Removed => removedColor, DiffLineType.Modified => modifiedColor, _ => Color.clear }; if (bgColor != Color.clear) { EditorGUI.DrawRect(lineRect, bgColor); } // Line prefix var prefix = line.type switch { DiffLineType.Added => "+ ", DiffLineType.Removed => "- ", DiffLineType.Modified => "~ ", _ => " " }; var displayText = prefix + line.content; if (!string.IsNullOrEmpty(searchText) && line.content.Contains(searchText)) { displayText = displayText.Replace(searchText, $"{searchText}"); } EditorGUI.LabelField(lineRect, displayText, codeStyle); } } } private void DrawVisualCompare() { using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { scrollPosition = scrollView.scrollPosition; if (currentDiff.diffType == DiffType.Image) { DrawImageOverlay(); } else { DrawSideBySideComparison(); } } } private void DrawImageOverlay() { DrawCard(() => { EditorGUILayout.LabelField("Image Overlay Comparison", EditorStyles.boldLabel); EditorGUILayout.Space(8); // TODO: Implement image overlay/blend comparison if (leftTexture != null && rightTexture != null) { var maxSize = 400f; var imageRect = GUILayoutUtility.GetRect(maxSize, maxSize); // For now, just show both images side by side var leftRect = new Rect(imageRect.x, imageRect.y, imageRect.width * 0.5f, imageRect.height); var rightRect = new Rect(imageRect.x + imageRect.width * 0.5f, imageRect.y, imageRect.width * 0.5f, imageRect.height); GUI.DrawTexture(leftRect, leftTexture, ScaleMode.ScaleToFit); GUI.DrawTexture(rightRect, rightTexture, ScaleMode.ScaleToFit); } else { EditorGUILayout.LabelField("Image overlay comparison not available", EditorStyles.centeredGreyMiniLabel); } }, 12); } private void DrawSideBySideComparison() { DrawSplitView(); // Fallback to split view for non-image assets } #endregion #region Helper Methods private void DrawCard(System.Action content, int padding = 12) { 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 List GenerateDiffLines(string leftContent, string rightContent) { var leftLines = leftContent.Split('\n'); var rightLines = rightContent.Split('\n'); var result = new List(); // Simple diff algorithm - in production, use a proper diff library var maxLines = Mathf.Max(leftLines.Length, rightLines.Length); for (int i = 0; i < maxLines; i++) { var leftLine = i < leftLines.Length ? leftLines[i] : null; var rightLine = i < rightLines.Length ? rightLines[i] : null; if (leftLine == null) { result.Add(new DiffLine { content = rightLine, type = DiffLineType.Added, leftLineNumber = 0, rightLineNumber = i + 1 }); } else if (rightLine == null) { result.Add(new DiffLine { content = leftLine, type = DiffLineType.Removed, leftLineNumber = i + 1, rightLineNumber = 0 }); } else if (leftLine == rightLine) { result.Add(new DiffLine { content = leftLine, type = DiffLineType.Context, leftLineNumber = i + 1, rightLineNumber = i + 1 }); } else { result.Add(new DiffLine { content = rightLine, type = DiffLineType.Modified, leftLineNumber = i + 1, rightLineNumber = i + 1 }); } } return result; } private DiffLine GetDiffLineForIndex(int index, bool isLeft) { if (currentDiff?.lines == null) return null; return currentDiff.lines.FirstOrDefault(line => isLeft ? line.leftLineNumber == index + 1 : line.rightLineNumber == index + 1); } private Texture2D CreateSolidColorTexture(Color color, int width, int height) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (int i = 0; i < pixels.Length; i++) pixels[i] = color; texture.SetPixels(pixels); texture.Apply(); return texture; } private void CleanupTextures() { foreach (var texture in textureCache.Values) { if (texture != null) DestroyImmediate(texture); } textureCache.Clear(); } #endregion } #region Data Models [Serializable] public class DiffData { public string filePath; public DiffType diffType; public string leftContent; public string rightContent; public string leftImagePath; public string rightImagePath; public List lines; public List propertyChanges; } [Serializable] public class DiffLine { public string content; public DiffLineType type; public int leftLineNumber; public int rightLineNumber; } [Serializable] public class PropertyChange { public string propertyName; public string oldValue; public string newValue; public PropertyChangeType changeType; } public enum DiffType { Text, Code, Properties, Image, Binary } public enum DiffLineType { Context, Added, Removed, Modified } public enum PropertyChangeType { Added, Removed, Modified } #endregion }