FileDiffViewer.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. namespace UnityVersionControl
  9. {
  10. /// <summary>
  11. /// Advanced File Diff Viewer for Unity Assets
  12. /// Supports text diffs, visual comparisons, and property changes
  13. /// </summary>
  14. public class FileDiffViewer : EditorWindow
  15. {
  16. #region Fields
  17. private Vector2 scrollPosition;
  18. private int selectedViewMode = 0;
  19. private readonly string[] viewModeNames = { "Split View", "Unified Diff", "Visual Compare" };
  20. private string currentFilePath = "";
  21. private DiffData currentDiff;
  22. private Texture2D leftTexture;
  23. private Texture2D rightTexture;
  24. // UI state
  25. private bool showLineNumbers = true;
  26. private bool showWhitespace = false;
  27. private bool wordWrap = false;
  28. private int contextLines = 3;
  29. private string searchText = "";
  30. // Visual constants
  31. private static readonly Color backgroundColor = new Color(0.22f, 0.22f, 0.22f);
  32. private static readonly Color cardColor = new Color(0.28f, 0.28f, 0.28f);
  33. private static readonly Color addedColor = new Color(0.2f, 0.6f, 0.2f, 0.3f);
  34. private static readonly Color removedColor = new Color(0.6f, 0.2f, 0.2f, 0.3f);
  35. private static readonly Color modifiedColor = new Color(0.6f, 0.4f, 0.2f, 0.3f);
  36. private static readonly Color lineNumberColor = new Color(0.5f, 0.5f, 0.5f);
  37. private static readonly Color contextColor = new Color(0.8f, 0.8f, 0.8f);
  38. // Caches
  39. private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
  40. private readonly Dictionary<string, DiffData> diffCache = new Dictionary<string, DiffData>();
  41. #endregion
  42. #region Unity Lifecycle
  43. [MenuItem("Window/Version Control/File Diff Viewer", false, 3)]
  44. public static void ShowWindow()
  45. {
  46. var window = GetWindow<FileDiffViewer>();
  47. window.titleContent = new GUIContent("File Diff");
  48. window.minSize = new Vector2(800, 600);
  49. }
  50. public static void ShowDiff(string filePath)
  51. {
  52. var window = GetWindow<FileDiffViewer>();
  53. window.titleContent = new GUIContent($"Diff: {System.IO.Path.GetFileName(filePath)}");
  54. window.minSize = new Vector2(800, 600);
  55. window.LoadFile(filePath);
  56. }
  57. private void OnEnable()
  58. {
  59. LoadTestData();
  60. }
  61. private void OnDisable()
  62. {
  63. CleanupTextures();
  64. }
  65. private void OnGUI()
  66. {
  67. DrawBackground();
  68. DrawToolbar();
  69. DrawDiffContent();
  70. }
  71. #endregion
  72. #region File Loading
  73. public void LoadFile(string filePath)
  74. {
  75. currentFilePath = filePath;
  76. if (diffCache.TryGetValue(filePath, out var cachedDiff))
  77. {
  78. currentDiff = cachedDiff;
  79. }
  80. else
  81. {
  82. currentDiff = GenerateDiff(filePath);
  83. diffCache[filePath] = currentDiff;
  84. }
  85. LoadAssetTextures(filePath);
  86. Repaint();
  87. }
  88. private void LoadTestData()
  89. {
  90. // Load a test diff for demonstration
  91. LoadFile("Assets/Scripts/PlayerController.cs");
  92. }
  93. private DiffData GenerateDiff(string filePath)
  94. {
  95. var extension = System.IO.Path.GetExtension(filePath).ToLower();
  96. switch (extension)
  97. {
  98. case ".cs":
  99. case ".js":
  100. case ".shader":
  101. case ".hlsl":
  102. case ".cginc":
  103. return GenerateCodeDiff(filePath);
  104. case ".unity":
  105. case ".prefab":
  106. case ".asset":
  107. case ".mat":
  108. return GenerateAssetDiff(filePath);
  109. case ".png":
  110. case ".jpg":
  111. case ".jpeg":
  112. case ".tga":
  113. return GenerateImageDiff(filePath);
  114. default:
  115. return GenerateTextDiff(filePath);
  116. }
  117. }
  118. private DiffData GenerateCodeDiff(string filePath)
  119. {
  120. // Mock C# code diff
  121. var diff = new DiffData
  122. {
  123. filePath = filePath,
  124. diffType = DiffType.Code,
  125. leftContent = GetMockCodeContent(true),
  126. rightContent = GetMockCodeContent(false),
  127. lines = new List<DiffLine>()
  128. };
  129. diff.lines = GenerateDiffLines(diff.leftContent, diff.rightContent);
  130. return diff;
  131. }
  132. private DiffData GenerateAssetDiff(string filePath)
  133. {
  134. // Mock Unity asset property diff
  135. var diff = new DiffData
  136. {
  137. filePath = filePath,
  138. diffType = DiffType.Properties,
  139. leftContent = GetMockAssetContent(true),
  140. rightContent = GetMockAssetContent(false),
  141. propertyChanges = GeneratePropertyChanges()
  142. };
  143. return diff;
  144. }
  145. private DiffData GenerateImageDiff(string filePath)
  146. {
  147. // Mock image diff
  148. var diff = new DiffData
  149. {
  150. filePath = filePath,
  151. diffType = DiffType.Image,
  152. leftImagePath = "Assets/Textures/PlayerTexture_Old.png",
  153. rightImagePath = "Assets/Textures/PlayerTexture_New.png"
  154. };
  155. return diff;
  156. }
  157. private DiffData GenerateTextDiff(string filePath)
  158. {
  159. // Generic text diff
  160. var diff = new DiffData
  161. {
  162. filePath = filePath,
  163. diffType = DiffType.Text,
  164. leftContent = "Original content line 1\nOriginal content line 2\nShared line 3\nOriginal line 4",
  165. rightContent = "Modified content line 1\nNew line 2\nShared line 3\nModified line 4\nNew line 5"
  166. };
  167. diff.lines = GenerateDiffLines(diff.leftContent, diff.rightContent);
  168. return diff;
  169. }
  170. private void LoadAssetTextures(string filePath)
  171. {
  172. if (currentDiff?.diffType == DiffType.Image)
  173. {
  174. leftTexture = LoadTextureFromPath(currentDiff.leftImagePath);
  175. rightTexture = LoadTextureFromPath(currentDiff.rightImagePath);
  176. }
  177. }
  178. private Texture2D LoadTextureFromPath(string path)
  179. {
  180. try
  181. {
  182. return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
  183. }
  184. catch
  185. {
  186. // Return a placeholder texture
  187. return CreateSolidColorTexture(Color.gray, 256, 256);
  188. }
  189. }
  190. #endregion
  191. #region Mock Data Generation
  192. private string GetMockCodeContent(bool isOld)
  193. {
  194. if (isOld)
  195. {
  196. return @"using UnityEngine;
  197. public class PlayerController : MonoBehaviour
  198. {
  199. public float speed = 5.0f;
  200. public float jumpForce = 8.0f;
  201. private Rigidbody rb;
  202. void Start()
  203. {
  204. rb = GetComponent<Rigidbody>();
  205. }
  206. void Update()
  207. {
  208. float horizontal = Input.GetAxis(""Horizontal"");
  209. float vertical = Input.GetAxis(""Vertical"");
  210. Vector3 movement = new Vector3(horizontal, 0, vertical);
  211. transform.Translate(movement * speed * Time.deltaTime);
  212. if (Input.GetKeyDown(KeyCode.Space))
  213. {
  214. rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
  215. }
  216. }
  217. }";
  218. }
  219. else
  220. {
  221. return @"using UnityEngine;
  222. public class PlayerController : MonoBehaviour
  223. {
  224. [Header(""Movement Settings"")]
  225. public float speed = 7.0f;
  226. public float jumpForce = 10.0f;
  227. public float groundCheckDistance = 0.1f;
  228. private Rigidbody rb;
  229. private bool isGrounded;
  230. void Start()
  231. {
  232. rb = GetComponent<Rigidbody>();
  233. }
  234. void Update()
  235. {
  236. CheckGrounded();
  237. HandleMovement();
  238. HandleJumping();
  239. }
  240. void CheckGrounded()
  241. {
  242. isGrounded = Physics.Raycast(transform.position, Vector3.down, groundCheckDistance);
  243. }
  244. void HandleMovement()
  245. {
  246. float horizontal = Input.GetAxis(""Horizontal"");
  247. float vertical = Input.GetAxis(""Vertical"");
  248. Vector3 movement = new Vector3(horizontal, 0, vertical).normalized;
  249. rb.velocity = new Vector3(movement.x * speed, rb.velocity.y, movement.z * speed);
  250. }
  251. void HandleJumping()
  252. {
  253. if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
  254. {
  255. rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
  256. }
  257. }
  258. }";
  259. }
  260. }
  261. private string GetMockAssetContent(bool isOld)
  262. {
  263. if (isOld)
  264. {
  265. return @"Transform:
  266. position: {x: 0, y: 0, z: 0}
  267. rotation: {x: 0, y: 0, z: 0, w: 1}
  268. scale: {x: 1, y: 1, z: 1}
  269. MeshRenderer:
  270. material: PlayerMaterial
  271. receiveShadows: 1
  272. BoxCollider:
  273. size: {x: 1, y: 1, z: 1}
  274. center: {x: 0, y: 0, z: 0}";
  275. }
  276. else
  277. {
  278. return @"Transform:
  279. position: {x: 0, y: 0.5, z: 0}
  280. rotation: {x: 0, y: 0, z: 0, w: 1}
  281. scale: {x: 1.2, y: 1.2, z: 1.2}
  282. MeshRenderer:
  283. material: PlayerMaterial_Updated
  284. receiveShadows: 1
  285. shadowCastingMode: 1
  286. BoxCollider:
  287. size: {x: 1.1, y: 1.1, z: 1.1}
  288. center: {x: 0, y: 0, z: 0}
  289. isTrigger: 0
  290. Rigidbody:
  291. mass: 1
  292. drag: 0.5";
  293. }
  294. }
  295. private List<PropertyChange> GeneratePropertyChanges()
  296. {
  297. return new List<PropertyChange>
  298. {
  299. new PropertyChange
  300. {
  301. propertyName = "Transform.position.y",
  302. oldValue = "0",
  303. newValue = "0.5",
  304. changeType = PropertyChangeType.Modified
  305. },
  306. new PropertyChange
  307. {
  308. propertyName = "Transform.scale",
  309. oldValue = "{x: 1, y: 1, z: 1}",
  310. newValue = "{x: 1.2, y: 1.2, z: 1.2}",
  311. changeType = PropertyChangeType.Modified
  312. },
  313. new PropertyChange
  314. {
  315. propertyName = "MeshRenderer.material",
  316. oldValue = "PlayerMaterial",
  317. newValue = "PlayerMaterial_Updated",
  318. changeType = PropertyChangeType.Modified
  319. },
  320. new PropertyChange
  321. {
  322. propertyName = "MeshRenderer.shadowCastingMode",
  323. oldValue = "",
  324. newValue = "1",
  325. changeType = PropertyChangeType.Added
  326. },
  327. new PropertyChange
  328. {
  329. propertyName = "BoxCollider.size",
  330. oldValue = "{x: 1, y: 1, z: 1}",
  331. newValue = "{x: 1.1, y: 1.1, z: 1.1}",
  332. changeType = PropertyChangeType.Modified
  333. },
  334. new PropertyChange
  335. {
  336. propertyName = "Rigidbody",
  337. oldValue = "",
  338. newValue = "Component Added",
  339. changeType = PropertyChangeType.Added
  340. }
  341. };
  342. }
  343. #endregion
  344. #region GUI Drawing
  345. private void DrawBackground()
  346. {
  347. var rect = new Rect(0, 0, position.width, position.height);
  348. EditorGUI.DrawRect(rect, backgroundColor);
  349. }
  350. private void DrawToolbar()
  351. {
  352. DrawCard(() =>
  353. {
  354. using (new EditorGUILayout.HorizontalScope())
  355. {
  356. // File info
  357. using (new EditorGUILayout.VerticalScope())
  358. {
  359. var titleStyle = new GUIStyle(EditorStyles.boldLabel);
  360. titleStyle.fontSize = 14;
  361. titleStyle.normal.textColor = Color.white;
  362. EditorGUILayout.LabelField(System.IO.Path.GetFileName(currentFilePath), titleStyle);
  363. var pathStyle = new GUIStyle(EditorStyles.label);
  364. pathStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  365. pathStyle.fontSize = 11;
  366. EditorGUILayout.LabelField(currentFilePath, pathStyle);
  367. }
  368. GUILayout.FlexibleSpace();
  369. // View mode selector
  370. EditorGUILayout.LabelField("View:", GUILayout.Width(40));
  371. selectedViewMode = EditorGUILayout.Popup(selectedViewMode, viewModeNames, GUILayout.Width(120));
  372. GUILayout.Space(8);
  373. // Options
  374. showLineNumbers = EditorGUILayout.Toggle("Lines", showLineNumbers, GUILayout.Width(60));
  375. showWhitespace = EditorGUILayout.Toggle("Space", showWhitespace, GUILayout.Width(60));
  376. wordWrap = EditorGUILayout.Toggle("Wrap", wordWrap, GUILayout.Width(60));
  377. }
  378. EditorGUILayout.Space(4);
  379. // Search bar
  380. using (new EditorGUILayout.HorizontalScope())
  381. {
  382. EditorGUILayout.LabelField("Search:", GUILayout.Width(50));
  383. searchText = EditorGUILayout.TextField(searchText);
  384. if (GUILayout.Button("Clear", GUILayout.Width(50)))
  385. searchText = "";
  386. }
  387. }, 12);
  388. }
  389. private void DrawDiffContent()
  390. {
  391. if (currentDiff == null)
  392. {
  393. DrawEmptyState();
  394. return;
  395. }
  396. EditorGUILayout.Space(8);
  397. switch (selectedViewMode)
  398. {
  399. case 0: DrawSplitView(); break;
  400. case 1: DrawUnifiedView(); break;
  401. case 2: DrawVisualCompare(); break;
  402. }
  403. }
  404. private void DrawEmptyState()
  405. {
  406. DrawCard(() =>
  407. {
  408. var iconRect = GUILayoutUtility.GetRect(48, 48);
  409. var emptyTexture = CreateSolidColorTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 48);
  410. GUI.DrawTexture(iconRect, emptyTexture);
  411. EditorGUILayout.Space(8);
  412. var titleStyle = new GUIStyle(EditorStyles.boldLabel);
  413. titleStyle.alignment = TextAnchor.MiddleCenter;
  414. titleStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  415. EditorGUILayout.LabelField("No File Selected", titleStyle);
  416. var subtitleStyle = new GUIStyle(EditorStyles.label);
  417. subtitleStyle.alignment = TextAnchor.MiddleCenter;
  418. subtitleStyle.normal.textColor = new Color(0.6f, 0.6f, 0.6f);
  419. subtitleStyle.wordWrap = true;
  420. EditorGUILayout.LabelField("Select a modified file to view its diff.", subtitleStyle);
  421. }, 32);
  422. }
  423. private void DrawSplitView()
  424. {
  425. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  426. {
  427. scrollPosition = scrollView.scrollPosition;
  428. switch (currentDiff.diffType)
  429. {
  430. case DiffType.Code:
  431. case DiffType.Text:
  432. DrawCodeSplitView();
  433. break;
  434. case DiffType.Properties:
  435. DrawPropertySplitView();
  436. break;
  437. case DiffType.Image:
  438. DrawImageSplitView();
  439. break;
  440. }
  441. }
  442. }
  443. private void DrawCodeSplitView()
  444. {
  445. DrawCard(() =>
  446. {
  447. using (new EditorGUILayout.HorizontalScope())
  448. {
  449. // Left panel header
  450. using (new EditorGUILayout.VerticalScope())
  451. {
  452. var headerStyle = new GUIStyle(EditorStyles.boldLabel);
  453. headerStyle.normal.textColor = removedColor;
  454. headerStyle.alignment = TextAnchor.MiddleCenter;
  455. EditorGUILayout.LabelField("BEFORE", headerStyle);
  456. DrawCodePanel(currentDiff.leftContent, true);
  457. }
  458. GUILayout.Space(8);
  459. // Right panel header
  460. using (new EditorGUILayout.VerticalScope())
  461. {
  462. var headerStyle = new GUIStyle(EditorStyles.boldLabel);
  463. headerStyle.normal.textColor = addedColor;
  464. headerStyle.alignment = TextAnchor.MiddleCenter;
  465. EditorGUILayout.LabelField("AFTER", headerStyle);
  466. DrawCodePanel(currentDiff.rightContent, false);
  467. }
  468. }
  469. }, 12);
  470. }
  471. private void DrawCodePanel(string content, bool isLeft)
  472. {
  473. var lines = content.Split('\n');
  474. var codeStyle = new GUIStyle(EditorStyles.textArea);
  475. codeStyle.wordWrap = wordWrap;
  476. codeStyle.richText = true;
  477. codeStyle.font = EditorGUIUtility.Load("Consolas") as Font ?? GUI.skin.font;
  478. for (int i = 0; i < lines.Length; i++)
  479. {
  480. using (new EditorGUILayout.HorizontalScope())
  481. {
  482. if (showLineNumbers)
  483. {
  484. var lineNumStyle = new GUIStyle(EditorStyles.label);
  485. lineNumStyle.normal.textColor = lineNumberColor;
  486. lineNumStyle.fontSize = 10;
  487. lineNumStyle.alignment = TextAnchor.MiddleRight;
  488. EditorGUILayout.LabelField((i + 1).ToString(), lineNumStyle, GUILayout.Width(30));
  489. }
  490. var line = lines[i];
  491. if (!string.IsNullOrEmpty(searchText) && line.Contains(searchText))
  492. {
  493. line = line.Replace(searchText, $"<color=yellow>{searchText}</color>");
  494. }
  495. var lineRect = EditorGUILayout.GetControlRect();
  496. // Highlight changed lines
  497. var diffLine = GetDiffLineForIndex(i, isLeft);
  498. if (diffLine != null)
  499. {
  500. Color bgColor = diffLine.type switch
  501. {
  502. DiffLineType.Added => addedColor,
  503. DiffLineType.Removed => removedColor,
  504. DiffLineType.Modified => modifiedColor,
  505. _ => Color.clear
  506. };
  507. if (bgColor != Color.clear)
  508. {
  509. EditorGUI.DrawRect(lineRect, bgColor);
  510. }
  511. }
  512. EditorGUI.LabelField(lineRect, line, codeStyle);
  513. }
  514. }
  515. }
  516. private void DrawPropertySplitView()
  517. {
  518. DrawCard(() =>
  519. {
  520. using (new EditorGUILayout.HorizontalScope())
  521. {
  522. // Properties comparison
  523. using (new EditorGUILayout.VerticalScope())
  524. {
  525. var headerStyle = new GUIStyle(EditorStyles.boldLabel);
  526. headerStyle.alignment = TextAnchor.MiddleCenter;
  527. EditorGUILayout.LabelField("PROPERTY CHANGES", headerStyle);
  528. EditorGUILayout.Space(8);
  529. foreach (var change in currentDiff.propertyChanges)
  530. {
  531. DrawPropertyChange(change);
  532. EditorGUILayout.Space(4);
  533. }
  534. }
  535. }
  536. }, 12);
  537. }
  538. private void DrawPropertyChange(PropertyChange change)
  539. {
  540. var changeRect = EditorGUILayout.BeginHorizontal();
  541. Color bgColor = change.changeType switch
  542. {
  543. PropertyChangeType.Added => addedColor,
  544. PropertyChangeType.Removed => removedColor,
  545. PropertyChangeType.Modified => modifiedColor,
  546. _ => Color.clear
  547. };
  548. if (bgColor != Color.clear)
  549. {
  550. EditorGUI.DrawRect(changeRect, bgColor);
  551. }
  552. GUILayout.Space(8);
  553. // Property name
  554. var nameStyle = new GUIStyle(EditorStyles.boldLabel);
  555. nameStyle.normal.textColor = Color.white;
  556. nameStyle.fontSize = 11;
  557. EditorGUILayout.LabelField(change.propertyName, nameStyle, GUILayout.Width(200));
  558. // Change indicator
  559. var changeIcon = change.changeType switch
  560. {
  561. PropertyChangeType.Added => "+",
  562. PropertyChangeType.Removed => "-",
  563. PropertyChangeType.Modified => "⟳",
  564. _ => "?"
  565. };
  566. var iconStyle = new GUIStyle(EditorStyles.label);
  567. iconStyle.normal.textColor = change.changeType switch
  568. {
  569. PropertyChangeType.Added => Color.green,
  570. PropertyChangeType.Removed => Color.red,
  571. PropertyChangeType.Modified => Color.yellow,
  572. _ => Color.white
  573. };
  574. EditorGUILayout.LabelField(changeIcon, iconStyle, GUILayout.Width(20));
  575. // Values
  576. using (new EditorGUILayout.VerticalScope())
  577. {
  578. if (!string.IsNullOrEmpty(change.oldValue))
  579. {
  580. var oldStyle = new GUIStyle(EditorStyles.label);
  581. oldStyle.normal.textColor = new Color(1f, 0.7f, 0.7f);
  582. oldStyle.fontSize = 10;
  583. EditorGUILayout.LabelField($"- {change.oldValue}", oldStyle);
  584. }
  585. if (!string.IsNullOrEmpty(change.newValue))
  586. {
  587. var newStyle = new GUIStyle(EditorStyles.label);
  588. newStyle.normal.textColor = new Color(0.7f, 1f, 0.7f);
  589. newStyle.fontSize = 10;
  590. EditorGUILayout.LabelField($"+ {change.newValue}", newStyle);
  591. }
  592. }
  593. GUILayout.Space(8);
  594. EditorGUILayout.EndHorizontal();
  595. }
  596. private void DrawImageSplitView()
  597. {
  598. DrawCard(() =>
  599. {
  600. using (new EditorGUILayout.HorizontalScope())
  601. {
  602. // Left image
  603. using (new EditorGUILayout.VerticalScope())
  604. {
  605. var headerStyle = new GUIStyle(EditorStyles.boldLabel);
  606. headerStyle.normal.textColor = removedColor;
  607. headerStyle.alignment = TextAnchor.MiddleCenter;
  608. EditorGUILayout.LabelField("BEFORE", headerStyle);
  609. DrawImagePanel(leftTexture, "Old Version");
  610. }
  611. GUILayout.Space(8);
  612. // Right image
  613. using (new EditorGUILayout.VerticalScope())
  614. {
  615. var headerStyle = new GUIStyle(EditorStyles.boldLabel);
  616. headerStyle.normal.textColor = addedColor;
  617. headerStyle.alignment = TextAnchor.MiddleCenter;
  618. EditorGUILayout.LabelField("AFTER", headerStyle);
  619. DrawImagePanel(rightTexture, "New Version");
  620. }
  621. }
  622. }, 12);
  623. }
  624. private void DrawImagePanel(Texture2D texture, string label)
  625. {
  626. if (texture != null)
  627. {
  628. var maxSize = 300f;
  629. var aspect = (float)texture.width / texture.height;
  630. var displayWidth = aspect > 1 ? maxSize : maxSize * aspect;
  631. var displayHeight = aspect > 1 ? maxSize / aspect : maxSize;
  632. var imageRect = GUILayoutUtility.GetRect(displayWidth, displayHeight);
  633. EditorGUI.DrawRect(imageRect, Color.black);
  634. GUI.DrawTexture(imageRect, texture, ScaleMode.ScaleToFit);
  635. // Image info
  636. var infoStyle = new GUIStyle(EditorStyles.label);
  637. infoStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  638. infoStyle.fontSize = 10;
  639. infoStyle.alignment = TextAnchor.MiddleCenter;
  640. EditorGUILayout.LabelField($"{texture.width}x{texture.height}", infoStyle);
  641. }
  642. else
  643. {
  644. var placeholderRect = GUILayoutUtility.GetRect(300, 200);
  645. EditorGUI.DrawRect(placeholderRect, new Color(0.3f, 0.3f, 0.3f));
  646. var labelStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  647. labelStyle.normal.textColor = Color.white;
  648. GUI.Label(placeholderRect, label, labelStyle);
  649. }
  650. }
  651. private void DrawUnifiedView()
  652. {
  653. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  654. {
  655. scrollPosition = scrollView.scrollPosition;
  656. DrawCard(() =>
  657. {
  658. if (currentDiff.lines != null && currentDiff.lines.Count > 0)
  659. {
  660. DrawUnifiedDiff();
  661. }
  662. else
  663. {
  664. var noChangesStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  665. noChangesStyle.normal.textColor = Color.white;
  666. EditorGUILayout.LabelField("No textual changes to display", noChangesStyle);
  667. }
  668. }, 12);
  669. }
  670. }
  671. private void DrawUnifiedDiff()
  672. {
  673. var codeStyle = new GUIStyle(EditorStyles.textArea);
  674. codeStyle.wordWrap = wordWrap;
  675. codeStyle.richText = true;
  676. codeStyle.font = EditorGUIUtility.Load("Consolas") as Font ?? GUI.skin.font;
  677. foreach (var line in currentDiff.lines)
  678. {
  679. using (new EditorGUILayout.HorizontalScope())
  680. {
  681. if (showLineNumbers)
  682. {
  683. var lineNumStyle = new GUIStyle(EditorStyles.label);
  684. lineNumStyle.normal.textColor = lineNumberColor;
  685. lineNumStyle.fontSize = 10;
  686. lineNumStyle.alignment = TextAnchor.MiddleRight;
  687. var lineNumber = line.type == DiffLineType.Removed ? line.leftLineNumber : line.rightLineNumber;
  688. EditorGUILayout.LabelField(lineNumber > 0 ? lineNumber.ToString() : "", lineNumStyle, GUILayout.Width(30));
  689. }
  690. var lineRect = EditorGUILayout.GetControlRect();
  691. // Background color based on line type
  692. Color bgColor = line.type switch
  693. {
  694. DiffLineType.Added => addedColor,
  695. DiffLineType.Removed => removedColor,
  696. DiffLineType.Modified => modifiedColor,
  697. _ => Color.clear
  698. };
  699. if (bgColor != Color.clear)
  700. {
  701. EditorGUI.DrawRect(lineRect, bgColor);
  702. }
  703. // Line prefix
  704. var prefix = line.type switch
  705. {
  706. DiffLineType.Added => "+ ",
  707. DiffLineType.Removed => "- ",
  708. DiffLineType.Modified => "~ ",
  709. _ => " "
  710. };
  711. var displayText = prefix + line.content;
  712. if (!string.IsNullOrEmpty(searchText) && line.content.Contains(searchText))
  713. {
  714. displayText = displayText.Replace(searchText, $"<color=yellow>{searchText}</color>");
  715. }
  716. EditorGUI.LabelField(lineRect, displayText, codeStyle);
  717. }
  718. }
  719. }
  720. private void DrawVisualCompare()
  721. {
  722. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  723. {
  724. scrollPosition = scrollView.scrollPosition;
  725. if (currentDiff.diffType == DiffType.Image)
  726. {
  727. DrawImageOverlay();
  728. }
  729. else
  730. {
  731. DrawSideBySideComparison();
  732. }
  733. }
  734. }
  735. private void DrawImageOverlay()
  736. {
  737. DrawCard(() =>
  738. {
  739. EditorGUILayout.LabelField("Image Overlay Comparison", EditorStyles.boldLabel);
  740. EditorGUILayout.Space(8);
  741. // TODO: Implement image overlay/blend comparison
  742. if (leftTexture != null && rightTexture != null)
  743. {
  744. var maxSize = 400f;
  745. var imageRect = GUILayoutUtility.GetRect(maxSize, maxSize);
  746. // For now, just show both images side by side
  747. var leftRect = new Rect(imageRect.x, imageRect.y, imageRect.width * 0.5f, imageRect.height);
  748. var rightRect = new Rect(imageRect.x + imageRect.width * 0.5f, imageRect.y, imageRect.width * 0.5f, imageRect.height);
  749. GUI.DrawTexture(leftRect, leftTexture, ScaleMode.ScaleToFit);
  750. GUI.DrawTexture(rightRect, rightTexture, ScaleMode.ScaleToFit);
  751. }
  752. else
  753. {
  754. EditorGUILayout.LabelField("Image overlay comparison not available", EditorStyles.centeredGreyMiniLabel);
  755. }
  756. }, 12);
  757. }
  758. private void DrawSideBySideComparison()
  759. {
  760. DrawSplitView(); // Fallback to split view for non-image assets
  761. }
  762. #endregion
  763. #region Helper Methods
  764. private void DrawCard(System.Action content, int padding = 12)
  765. {
  766. var rect = EditorGUILayout.BeginVertical();
  767. EditorGUI.DrawRect(rect, cardColor);
  768. GUILayout.Space(padding);
  769. EditorGUILayout.BeginHorizontal();
  770. GUILayout.Space(padding);
  771. EditorGUILayout.BeginVertical();
  772. content?.Invoke();
  773. EditorGUILayout.EndVertical();
  774. GUILayout.Space(padding);
  775. EditorGUILayout.EndHorizontal();
  776. GUILayout.Space(padding);
  777. EditorGUILayout.EndVertical();
  778. }
  779. private List<DiffLine> GenerateDiffLines(string leftContent, string rightContent)
  780. {
  781. var leftLines = leftContent.Split('\n');
  782. var rightLines = rightContent.Split('\n');
  783. var result = new List<DiffLine>();
  784. // Simple diff algorithm - in production, use a proper diff library
  785. var maxLines = Mathf.Max(leftLines.Length, rightLines.Length);
  786. for (int i = 0; i < maxLines; i++)
  787. {
  788. var leftLine = i < leftLines.Length ? leftLines[i] : null;
  789. var rightLine = i < rightLines.Length ? rightLines[i] : null;
  790. if (leftLine == null)
  791. {
  792. result.Add(new DiffLine
  793. {
  794. content = rightLine,
  795. type = DiffLineType.Added,
  796. leftLineNumber = 0,
  797. rightLineNumber = i + 1
  798. });
  799. }
  800. else if (rightLine == null)
  801. {
  802. result.Add(new DiffLine
  803. {
  804. content = leftLine,
  805. type = DiffLineType.Removed,
  806. leftLineNumber = i + 1,
  807. rightLineNumber = 0
  808. });
  809. }
  810. else if (leftLine == rightLine)
  811. {
  812. result.Add(new DiffLine
  813. {
  814. content = leftLine,
  815. type = DiffLineType.Context,
  816. leftLineNumber = i + 1,
  817. rightLineNumber = i + 1
  818. });
  819. }
  820. else
  821. {
  822. result.Add(new DiffLine
  823. {
  824. content = rightLine,
  825. type = DiffLineType.Modified,
  826. leftLineNumber = i + 1,
  827. rightLineNumber = i + 1
  828. });
  829. }
  830. }
  831. return result;
  832. }
  833. private DiffLine GetDiffLineForIndex(int index, bool isLeft)
  834. {
  835. if (currentDiff?.lines == null) return null;
  836. return currentDiff.lines.FirstOrDefault(line =>
  837. isLeft ? line.leftLineNumber == index + 1 : line.rightLineNumber == index + 1);
  838. }
  839. private Texture2D CreateSolidColorTexture(Color color, int width, int height)
  840. {
  841. var texture = new Texture2D(width, height);
  842. var pixels = new Color[width * height];
  843. for (int i = 0; i < pixels.Length; i++)
  844. pixels[i] = color;
  845. texture.SetPixels(pixels);
  846. texture.Apply();
  847. return texture;
  848. }
  849. private void CleanupTextures()
  850. {
  851. foreach (var texture in textureCache.Values)
  852. {
  853. if (texture != null) DestroyImmediate(texture);
  854. }
  855. textureCache.Clear();
  856. }
  857. #endregion
  858. }
  859. #region Data Models
  860. [Serializable]
  861. public class DiffData
  862. {
  863. public string filePath;
  864. public DiffType diffType;
  865. public string leftContent;
  866. public string rightContent;
  867. public string leftImagePath;
  868. public string rightImagePath;
  869. public List<DiffLine> lines;
  870. public List<PropertyChange> propertyChanges;
  871. }
  872. [Serializable]
  873. public class DiffLine
  874. {
  875. public string content;
  876. public DiffLineType type;
  877. public int leftLineNumber;
  878. public int rightLineNumber;
  879. }
  880. [Serializable]
  881. public class PropertyChange
  882. {
  883. public string propertyName;
  884. public string oldValue;
  885. public string newValue;
  886. public PropertyChangeType changeType;
  887. }
  888. public enum DiffType
  889. {
  890. Text,
  891. Code,
  892. Properties,
  893. Image,
  894. Binary
  895. }
  896. public enum DiffLineType
  897. {
  898. Context,
  899. Added,
  900. Removed,
  901. Modified
  902. }
  903. public enum PropertyChangeType
  904. {
  905. Added,
  906. Removed,
  907. Modified
  908. }
  909. #endregion
  910. }