From f4cb0c4b1f7a05d074800a05ab77161d9024f1eb Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Tue, 10 Feb 2026 18:31:51 +0100 Subject: [PATCH] file comparison improved --- .../kfmanager/ui/FileComparisonDialog.java | 121 ++++++++++++++++-- 1 file changed, 113 insertions(+), 8 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java b/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java index 1702420..14b88e2 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FileComparisonDialog.java @@ -20,6 +20,8 @@ public class FileComparisonDialog extends JFrame { private final AppConfig config; private JTextPane textPane1; private JTextPane textPane2; + private JTextArea lineNumbers1; + private JTextArea lineNumbers2; private JScrollPane scroll1; private JScrollPane scroll2; private List lines1 = new ArrayList<>(); @@ -34,6 +36,7 @@ public class FileComparisonDialog extends JFrame { try { this.lines1 = readLines(file1); this.lines2 = readLines(file2); + performSmartAlignment(); } catch (IOException e) { JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } @@ -66,6 +69,14 @@ public class FileComparisonDialog extends JFrame { TextPaneListener listener1 = new TextPaneListener(); textPane1.addMouseListener(listener1); scroll1 = new JScrollPane(textPane1); + + lineNumbers1 = new JTextArea("1"); + lineNumbers1.setEditable(false); + lineNumbers1.setBackground(Color.LIGHT_GRAY); + lineNumbers1.setForeground(Color.DARK_GRAY); + lineNumbers1.setFont(new Font("Monospaced", Font.PLAIN, 12)); + lineNumbers1.setMargin(new Insets(0, 5, 0, 5)); + scroll1.setRowHeaderView(lineNumbers1); textPane2 = new JTextPane(); textPane2.setEditable(false); @@ -74,6 +85,14 @@ public class FileComparisonDialog extends JFrame { textPane2.addMouseListener(listener2); scroll2 = new JScrollPane(textPane2); + lineNumbers2 = new JTextArea("1"); + lineNumbers2.setEditable(false); + lineNumbers2.setBackground(Color.LIGHT_GRAY); + lineNumbers2.setForeground(Color.DARK_GRAY); + lineNumbers2.setFont(new Font("Monospaced", Font.PLAIN, 12)); + lineNumbers2.setMargin(new Insets(0, 5, 0, 5)); + scroll2.setRowHeaderView(lineNumbers2); + // Synchronize scrolling scroll1.getVerticalScrollBar().setModel(scroll2.getVerticalScrollBar().getModel()); @@ -153,12 +172,12 @@ public class FileComparisonDialog extends JFrame { if (line1 < line2) { int diff = line2 - line1; for (int i = 0; i < diff; i++) { - lines1.add(line1, ""); + lines1.add(line1, null); } } else if (line2 < line1) { int diff = line1 - line2; for (int i = 0; i < diff; i++) { - lines2.add(line2, ""); + lines2.add(line2, null); } } updateDisplay(false); @@ -198,6 +217,15 @@ public class FileComparisonDialog extends JFrame { textPane2.setForeground(dark ? Color.WHITE : Color.BLACK); textPane1.setCaretColor(dark ? Color.WHITE : Color.BLACK); textPane2.setCaretColor(dark ? Color.WHITE : Color.BLACK); + + if (lineNumbers1 != null) { + lineNumbers1.setBackground(dark ? bg.brighter() : bg.darker()); + lineNumbers1.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY); + } + if (lineNumbers2 != null) { + lineNumbers2.setBackground(dark ? bg.brighter() : bg.darker()); + lineNumbers2.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY); + } } Font f = config.getGlobalFont(); if (f != null) { @@ -205,6 +233,8 @@ public class FileComparisonDialog extends JFrame { Font mono = new Font("Monospaced", Font.PLAIN, f.getSize()); textPane1.setFont(mono); textPane2.setFont(mono); + if (lineNumbers1 != null) lineNumbers1.setFont(mono); + if (lineNumbers2 != null) lineNumbers2.setFont(mono); } } @@ -222,6 +252,8 @@ public class FileComparisonDialog extends JFrame { textPane2.setText(""); StyledDocument doc1 = textPane1.getStyledDocument(); StyledDocument doc2 = textPane2.getStyledDocument(); + StringBuilder ln1 = new StringBuilder(); + StringBuilder ln2 = new StringBuilder(); Style diffStyle = textPane1.addStyle("diff", null); Color bg = textPane1.getBackground(); @@ -230,16 +262,35 @@ public class FileComparisonDialog extends JFrame { StyleConstants.setForeground(diffStyle, dark ? Color.WHITE : Color.BLACK); int maxLines = Math.max(lines1.size(), lines2.size()); - for (int i = 0; i < maxLines; i++) { - String l1 = i < lines1.size() ? lines1.get(i) : ""; - String l2 = i < lines2.size() ? lines2.get(i) : ""; + int counter1 = 1; + int counter2 = 1; - boolean different = !l1.equals(l2); + for (int i = 0; i < maxLines; i++) { + String l1 = i < lines1.size() ? lines1.get(i) : null; + String l2 = i < lines2.size() ? lines2.get(i) : null; + + boolean different = (l1 == null && l2 != null) || (l1 != null && l2 == null) || (l1 != null && l2 != null && !l1.equals(l2)); Style s = different ? diffStyle : null; - doc1.insertString(doc1.getLength(), l1 + "\n", s); - doc2.insertString(doc2.getLength(), l2 + "\n", s); + if (l1 != null) { + doc1.insertString(doc1.getLength(), l1 + "\n", s); + ln1.append(counter1++).append("\n"); + } else { + doc1.insertString(doc1.getLength(), "\n", s); + ln1.append("\n"); + } + + if (l2 != null) { + doc2.insertString(doc2.getLength(), l2 + "\n", s); + ln2.append(counter2++).append("\n"); + } else { + doc2.insertString(doc2.getLength(), "\n", s); + ln2.append("\n"); + } } + + if (lineNumbers1 != null) lineNumbers1.setText(ln1.toString()); + if (lineNumbers2 != null) lineNumbers2.setText(ln2.toString()); if (resetCaret) { textPane1.setCaretPosition(0); @@ -250,6 +301,60 @@ public class FileComparisonDialog extends JFrame { } } + private void performSmartAlignment() { + if (lines1.isEmpty() || lines2.isEmpty()) return; + + // Skip alignment for very large files to avoid O(N^2) memory/time issues + if (lines1.size() > 2000 || lines2.size() > 2000) return; + + int n = lines1.size(); + int m = lines2.size(); + int[][] dp = new int[n + 1][m + 1]; + + // LCS computation with similarity check (exact or trimmed match) + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + String s1 = lines1.get(i - 1); + String s2 = lines2.get(j - 1); + if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + List aligned1 = new ArrayList<>(); + List aligned2 = new ArrayList<>(); + int i = n, j = m; + while (i > 0 || j > 0) { + if (i > 0 && j > 0) { + String s1 = lines1.get(i - 1); + String s2 = lines2.get(j - 1); + if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) { + aligned1.add(0, s1); + aligned2.add(0, s2); + i--; + j--; + continue; + } + } + + if (j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j])) { + aligned1.add(0, null); // Added gap marker + aligned2.add(0, lines2.get(j - 1)); + j--; + } else if (i > 0) { + aligned1.add(0, lines1.get(i - 1)); + aligned2.add(0, null); // Added gap marker + i--; + } + } + + this.lines1 = aligned1; + this.lines2 = aligned2; + } + private List readLines(File f) throws IOException { List lines = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(f))) {