better file compare
This commit is contained in:
parent
bcfa68f30d
commit
561beb1e3e
@ -1,54 +1,81 @@
|
|||||||
package cz.kamma.kfmanager.ui;
|
package cz.kamma.kfmanager.ui;
|
||||||
|
|
||||||
import cz.kamma.kfmanager.config.AppConfig;
|
import cz.kamma.kfmanager.config.AppConfig;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.text.*;
|
import javax.swing.text.BadLocationException;
|
||||||
|
import javax.swing.BoundedRangeModel;
|
||||||
|
import javax.swing.text.Element;
|
||||||
|
import javax.swing.text.Style;
|
||||||
|
import javax.swing.text.StyleConstants;
|
||||||
|
import javax.swing.text.StyledDocument;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Window for comparing two files side by side
|
* Window for comparing two files side by side.
|
||||||
*/
|
*/
|
||||||
public class FileComparisonDialog extends JFrame {
|
public class FileComparisonDialog extends JFrame {
|
||||||
private final AppConfig config;
|
private final AppConfig config;
|
||||||
|
private final File file1;
|
||||||
|
private final File file2;
|
||||||
|
|
||||||
|
private final List<String> sourceLines1 = new ArrayList<>();
|
||||||
|
private final List<String> sourceLines2 = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<AlignedLine> alignedLines = new ArrayList<>();
|
||||||
|
private List<Integer> visibleIndices = new ArrayList<>();
|
||||||
|
private List<Integer> differenceVisibleRows = new ArrayList<>();
|
||||||
|
|
||||||
private JTextPane textPane1;
|
private JTextPane textPane1;
|
||||||
private JTextPane textPane2;
|
private JTextPane textPane2;
|
||||||
private JTextArea lineNumbers1;
|
private JTextArea lineNumbers1;
|
||||||
private JTextArea lineNumbers2;
|
private JTextArea lineNumbers2;
|
||||||
private JScrollPane scroll1;
|
private JScrollPane scroll1;
|
||||||
private JScrollPane scroll2;
|
private JScrollPane scroll2;
|
||||||
private List<String> lines1 = new ArrayList<>();
|
|
||||||
private List<String> lines2 = new ArrayList<>();
|
private JCheckBox smartAlignCheck;
|
||||||
private int selectedLine1 = 0;
|
private JCheckBox ignoreCaseCheck;
|
||||||
private int selectedLine2 = 0;
|
private JCheckBox ignoreTrimCheck;
|
||||||
|
private JCheckBox ignoreWhitespaceCheck;
|
||||||
|
private JCheckBox onlyDifferencesCheck;
|
||||||
|
private JLabel statusLabel;
|
||||||
|
|
||||||
|
private int selectedVisibleRow = -1;
|
||||||
|
private int selectedLeftVisibleRow = -1;
|
||||||
|
private int selectedRightVisibleRow = -1;
|
||||||
|
private int selectedDifferencePointer = -1;
|
||||||
|
private int manualAnchorLeftLine = -1;
|
||||||
|
private int manualAnchorRightLine = -1;
|
||||||
|
|
||||||
public FileComparisonDialog(Window parent, File file1, File file2, AppConfig config) {
|
public FileComparisonDialog(Window parent, File file1, File file2, AppConfig config) {
|
||||||
super("Compare Files: " + file1.getName() + " vs " + file2.getName());
|
super("Compare Files: " + file1.getName() + " vs " + file2.getName());
|
||||||
|
this.file1 = file1;
|
||||||
|
this.file2 = file2;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.lines1 = readLines(file1);
|
sourceLines1.addAll(readLines(file1));
|
||||||
this.lines2 = readLines(file2);
|
sourceLines2.addAll(readLines(file2));
|
||||||
performSmartAlignment();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
JOptionPane.showMessageDialog(parent, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
initComponents();
|
initComponents();
|
||||||
updateDisplay();
|
refreshComparison(true);
|
||||||
|
|
||||||
setSize(1000, 700);
|
setSize(1100, 760);
|
||||||
setLocationRelativeTo(parent);
|
setLocationRelativeTo(parent);
|
||||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||||
|
|
||||||
// Close on ESC
|
|
||||||
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close");
|
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close");
|
||||||
getRootPane().getActionMap().put("close", new AbstractAction() {
|
getRootPane().getActionMap().put("close", new AbstractAction() {
|
||||||
@Override
|
@Override
|
||||||
@ -59,152 +86,373 @@ public class FileComparisonDialog extends JFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initComponents() {
|
private void initComponents() {
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout(6, 6));
|
||||||
|
add(createTopToolbar(), BorderLayout.NORTH);
|
||||||
JPanel centerPanel = new JPanel(new GridLayout(1, 2, 5, 0));
|
add(createCenterPanel(), BorderLayout.CENTER);
|
||||||
|
add(createBottomBar(), BorderLayout.SOUTH);
|
||||||
textPane1 = new JTextPane();
|
|
||||||
textPane1.setEditable(false);
|
|
||||||
textPane1.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
|
||||||
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);
|
|
||||||
textPane2.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
|
||||||
TextPaneListener listener2 = new TextPaneListener();
|
|
||||||
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());
|
|
||||||
|
|
||||||
centerPanel.add(scroll1);
|
|
||||||
centerPanel.add(scroll2);
|
|
||||||
|
|
||||||
add(centerPanel, BorderLayout.CENTER);
|
|
||||||
|
|
||||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
|
||||||
JButton syncButton = new JButton("Synchronize from here");
|
|
||||||
syncButton.addActionListener(e -> synchronizeFromHere());
|
|
||||||
bottomPanel.add(syncButton);
|
|
||||||
|
|
||||||
JButton closeButton = new JButton("Close");
|
|
||||||
closeButton.addActionListener(e -> dispose());
|
|
||||||
bottomPanel.add(closeButton);
|
|
||||||
add(bottomPanel, BorderLayout.SOUTH);
|
|
||||||
|
|
||||||
// Apply appearance from config
|
|
||||||
applyAppearance();
|
applyAppearance();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextPaneListener extends java.awt.event.MouseAdapter {
|
private JComponent createTopToolbar() {
|
||||||
@Override
|
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4));
|
||||||
public void mousePressed(java.awt.event.MouseEvent e) {
|
|
||||||
handleSelection(e);
|
|
||||||
if (e.isPopupTrigger()) showMenu(e);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void mouseReleased(java.awt.event.MouseEvent e) {
|
|
||||||
handleSelection(e);
|
|
||||||
if (e.isPopupTrigger()) showMenu(e);
|
|
||||||
}
|
|
||||||
private void handleSelection(java.awt.event.MouseEvent e) {
|
|
||||||
if (SwingUtilities.isLeftMouseButton(e)) {
|
|
||||||
JTextPane pane = (JTextPane) e.getComponent();
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
int pos = pane.viewToModel(e.getPoint());
|
|
||||||
if (pos >= 0) {
|
|
||||||
try {
|
|
||||||
Element root = pane.getDocument().getDefaultRootElement();
|
|
||||||
int lineIdx = root.getElementIndex(pos);
|
|
||||||
|
|
||||||
if (pane == textPane1) selectedLine1 = lineIdx;
|
smartAlignCheck = new JCheckBox("Smart align", true);
|
||||||
else if (pane == textPane2) selectedLine2 = lineIdx;
|
ignoreCaseCheck = new JCheckBox("Ignore case", false);
|
||||||
|
ignoreTrimCheck = new JCheckBox("Ignore leading/trailing spaces", false);
|
||||||
|
ignoreWhitespaceCheck = new JCheckBox("Ignore all whitespace changes", false);
|
||||||
|
onlyDifferencesCheck = new JCheckBox("Show only differences", false);
|
||||||
|
|
||||||
Element line = root.getElement(lineIdx);
|
JButton prevDiff = new JButton("Previous difference");
|
||||||
int start = line.getStartOffset();
|
JButton nextDiff = new JButton("Next difference");
|
||||||
int end = Math.min(line.getEndOffset(), pane.getDocument().getLength());
|
JButton synchronizeButton = new JButton("Synchronize from selected rows");
|
||||||
|
JButton clearSyncButton = new JButton("Clear sync");
|
||||||
|
JButton reloadButton = new JButton("Reload");
|
||||||
|
|
||||||
// Use invokeLater to ensure selection happens after default UI behavior
|
smartAlignCheck.addActionListener(e -> refreshComparison(false));
|
||||||
SwingUtilities.invokeLater(() -> {
|
ignoreCaseCheck.addActionListener(e -> refreshComparison(false));
|
||||||
pane.requestFocusInWindow();
|
ignoreTrimCheck.addActionListener(e -> refreshComparison(false));
|
||||||
pane.setCaretPosition(start);
|
ignoreWhitespaceCheck.addActionListener(e -> refreshComparison(false));
|
||||||
pane.moveCaretPosition(end);
|
onlyDifferencesCheck.addActionListener(e -> refreshComparison(true));
|
||||||
});
|
reloadButton.addActionListener(e -> reloadAndRefresh());
|
||||||
} catch (Exception ex) {
|
|
||||||
// ignore
|
prevDiff.addActionListener(e -> jumpToDifference(-1));
|
||||||
}
|
nextDiff.addActionListener(e -> jumpToDifference(1));
|
||||||
}
|
synchronizeButton.addActionListener(e -> synchronizeFromSelectedRows());
|
||||||
|
clearSyncButton.addActionListener(e -> clearSynchronization());
|
||||||
|
|
||||||
|
panel.add(new JLabel("Options:"));
|
||||||
|
panel.add(smartAlignCheck);
|
||||||
|
panel.add(ignoreCaseCheck);
|
||||||
|
panel.add(ignoreTrimCheck);
|
||||||
|
panel.add(ignoreWhitespaceCheck);
|
||||||
|
panel.add(onlyDifferencesCheck);
|
||||||
|
panel.add(prevDiff);
|
||||||
|
panel.add(nextDiff);
|
||||||
|
panel.add(synchronizeButton);
|
||||||
|
panel.add(clearSyncButton);
|
||||||
|
panel.add(reloadButton);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComponent createCenterPanel() {
|
||||||
|
JPanel panel = new JPanel(new GridLayout(1, 2, 6, 0));
|
||||||
|
|
||||||
|
textPane1 = createTextPane(true);
|
||||||
|
textPane2 = createTextPane(false);
|
||||||
|
scroll1 = new JScrollPane(textPane1);
|
||||||
|
scroll2 = new JScrollPane(textPane2);
|
||||||
|
|
||||||
|
lineNumbers1 = createLineNumberArea();
|
||||||
|
lineNumbers2 = createLineNumberArea();
|
||||||
|
scroll1.setRowHeaderView(lineNumbers1);
|
||||||
|
scroll2.setRowHeaderView(lineNumbers2);
|
||||||
|
|
||||||
|
// Keep vertical scrolling synchronized in both directions.
|
||||||
|
BoundedRangeModel sharedModel = scroll1.getVerticalScrollBar().getModel();
|
||||||
|
scroll2.getVerticalScrollBar().setModel(sharedModel);
|
||||||
|
|
||||||
|
panel.add(scroll1);
|
||||||
|
panel.add(scroll2);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JComponent createBottomBar() {
|
||||||
|
JPanel bar = new JPanel(new BorderLayout(8, 0));
|
||||||
|
statusLabel = new JLabel(" ");
|
||||||
|
bar.add(statusLabel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
|
||||||
|
JButton closeButton = new JButton("Close");
|
||||||
|
closeButton.addActionListener(e -> dispose());
|
||||||
|
buttons.add(closeButton);
|
||||||
|
bar.add(buttons, BorderLayout.EAST);
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JTextPane createTextPane(boolean isLeftPane) {
|
||||||
|
JTextPane pane = new JTextPane();
|
||||||
|
pane.setEditable(false);
|
||||||
|
pane.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||||
|
pane.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mousePressed(java.awt.event.MouseEvent e) {
|
||||||
|
selectLineAtClick((JTextPane) e.getComponent(), e.getPoint(), isLeftPane);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JTextArea createLineNumberArea() {
|
||||||
|
JTextArea area = new JTextArea("1");
|
||||||
|
area.setEditable(false);
|
||||||
|
area.setBackground(Color.LIGHT_GRAY);
|
||||||
|
area.setForeground(Color.DARK_GRAY);
|
||||||
|
area.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||||
|
area.setMargin(new Insets(0, 5, 0, 5));
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadAndRefresh() {
|
||||||
|
sourceLines1.clear();
|
||||||
|
sourceLines2.clear();
|
||||||
|
try {
|
||||||
|
sourceLines1.addAll(readLines(file1));
|
||||||
|
sourceLines2.addAll(readLines(file2));
|
||||||
|
} catch (IOException e) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Error reading files: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
private void showMenu(java.awt.event.MouseEvent e) {
|
refreshComparison(true);
|
||||||
JPopupMenu menu = new JPopupMenu();
|
}
|
||||||
JMenuItem syncItem = new JMenuItem("Synchronize from here");
|
|
||||||
syncItem.addActionListener(event -> synchronizeFromHere());
|
private void synchronizeFromSelectedRows() {
|
||||||
menu.add(syncItem);
|
if (selectedLeftVisibleRow < 0 || selectedRightVisibleRow < 0) {
|
||||||
menu.show(e.getComponent(), e.getX(), e.getY());
|
JOptionPane.showMessageDialog(this,
|
||||||
|
"Select one line in left pane and one line in right pane first.",
|
||||||
|
"Synchronize",
|
||||||
|
JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedLeftVisibleRow >= visibleIndices.size() || selectedRightVisibleRow >= visibleIndices.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlignedLine leftLine = alignedLines.get(visibleIndices.get(selectedLeftVisibleRow));
|
||||||
|
AlignedLine rightLine = alignedLines.get(visibleIndices.get(selectedRightVisibleRow));
|
||||||
|
|
||||||
|
if (leftLine.leftNumber <= 0 || rightLine.rightNumber <= 0) {
|
||||||
|
JOptionPane.showMessageDialog(this,
|
||||||
|
"Selected rows must contain real lines in both panes (not empty gap rows).",
|
||||||
|
"Synchronize",
|
||||||
|
JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualAnchorLeftLine = leftLine.leftNumber;
|
||||||
|
manualAnchorRightLine = rightLine.rightNumber;
|
||||||
|
refreshComparison(false);
|
||||||
|
focusAnchorRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearSynchronization() {
|
||||||
|
manualAnchorLeftLine = -1;
|
||||||
|
manualAnchorRightLine = -1;
|
||||||
|
refreshComparison(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void focusAnchorRow() {
|
||||||
|
if (manualAnchorLeftLine <= 0 || manualAnchorRightLine <= 0) return;
|
||||||
|
for (int visibleRow = 0; visibleRow < visibleIndices.size(); visibleRow++) {
|
||||||
|
AlignedLine line = alignedLines.get(visibleIndices.get(visibleRow));
|
||||||
|
if (line.leftNumber == manualAnchorLeftLine && line.rightNumber == manualAnchorRightLine) {
|
||||||
|
selectedVisibleRow = visibleRow;
|
||||||
|
selectedLeftVisibleRow = visibleRow;
|
||||||
|
selectedRightVisibleRow = visibleRow;
|
||||||
|
render();
|
||||||
|
updateStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void synchronizeFromHere() {
|
private void refreshComparison(boolean resetSelection) {
|
||||||
try {
|
ComparisonOptions options = getOptions();
|
||||||
int line1 = selectedLine1;
|
List<String> comparableLeft = buildComparableLines(sourceLines1, options);
|
||||||
int line2 = selectedLine2;
|
List<String> comparableRight = buildComparableLines(sourceLines2, options);
|
||||||
|
alignedLines = alignWithOptionalAnchor(sourceLines1, sourceLines2, comparableLeft, comparableRight, options);
|
||||||
|
|
||||||
if (line1 < line2) {
|
visibleIndices = new ArrayList<>();
|
||||||
int diff = line2 - line1;
|
differenceVisibleRows = new ArrayList<>();
|
||||||
for (int i = 0; i < diff; i++) {
|
|
||||||
lines1.add(line1, null);
|
for (int i = 0; i < alignedLines.size(); i++) {
|
||||||
}
|
AlignedLine line = alignedLines.get(i);
|
||||||
} else if (line2 < line1) {
|
if (!options.onlyDifferences || line.different) {
|
||||||
int diff = line1 - line2;
|
int visibleRow = visibleIndices.size();
|
||||||
for (int i = 0; i < diff; i++) {
|
visibleIndices.add(i);
|
||||||
lines2.add(line2, null);
|
if (line.different) differenceVisibleRows.add(visibleRow);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateDisplay(false);
|
|
||||||
|
|
||||||
// Restore selection and scroll to the newly synced line
|
|
||||||
int newLineIdx = Math.max(line1, line2);
|
|
||||||
selectedLine1 = newLineIdx;
|
|
||||||
selectedLine2 = newLineIdx;
|
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
Element newRoot = textPane1.getDocument().getDefaultRootElement();
|
|
||||||
if (newLineIdx < newRoot.getElementCount()) {
|
|
||||||
Element lineElem = newRoot.getElement(newLineIdx);
|
|
||||||
int start = lineElem.getStartOffset();
|
|
||||||
int end = Math.min(lineElem.getEndOffset(), textPane1.getDocument().getLength());
|
|
||||||
|
|
||||||
textPane1.requestFocusInWindow();
|
|
||||||
textPane1.setCaretPosition(start);
|
|
||||||
textPane1.moveCaretPosition(end);
|
|
||||||
|
|
||||||
textPane2.setCaretPosition(start);
|
|
||||||
textPane2.moveCaretPosition(end);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resetSelection) {
|
||||||
|
selectedVisibleRow = -1;
|
||||||
|
selectedLeftVisibleRow = -1;
|
||||||
|
selectedRightVisibleRow = -1;
|
||||||
|
selectedDifferencePointer = differenceVisibleRows.isEmpty() ? -1 : 0;
|
||||||
|
} else if (selectedVisibleRow >= visibleIndices.size()) {
|
||||||
|
selectedVisibleRow = visibleIndices.isEmpty() ? -1 : visibleIndices.size() - 1;
|
||||||
|
selectedLeftVisibleRow = selectedVisibleRow;
|
||||||
|
selectedRightVisibleRow = selectedVisibleRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComparisonOptions getOptions() {
|
||||||
|
ComparisonOptions options = new ComparisonOptions();
|
||||||
|
options.smartAlign = smartAlignCheck.isSelected();
|
||||||
|
options.ignoreCase = ignoreCaseCheck.isSelected();
|
||||||
|
options.ignoreTrim = ignoreTrimCheck.isSelected();
|
||||||
|
options.ignoreWhitespace = ignoreWhitespaceCheck.isSelected();
|
||||||
|
options.onlyDifferences = onlyDifferencesCheck.isSelected();
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render() {
|
||||||
|
try {
|
||||||
|
StyledDocument doc1 = textPane1.getStyledDocument();
|
||||||
|
StyledDocument doc2 = textPane2.getStyledDocument();
|
||||||
|
doc1.remove(0, doc1.getLength());
|
||||||
|
doc2.remove(0, doc2.getLength());
|
||||||
|
|
||||||
|
StringBuilder ln1 = new StringBuilder();
|
||||||
|
StringBuilder ln2 = new StringBuilder();
|
||||||
|
|
||||||
|
Color bg = textPane1.getBackground();
|
||||||
|
boolean dark = isDark(bg);
|
||||||
|
Style normal1 = createStyle(textPane1, "normal1", null, null);
|
||||||
|
Style normal2 = createStyle(textPane2, "normal2", null, null);
|
||||||
|
Style changed1 = createStyle(textPane1, "changed1", dark ? new Color(95, 45, 45) : new Color(255, 220, 220), null);
|
||||||
|
Style changed2 = createStyle(textPane2, "changed2", dark ? new Color(95, 45, 45) : new Color(255, 220, 220), null);
|
||||||
|
Style onlyLeft = createStyle(textPane1, "onlyLeft", dark ? new Color(85, 62, 40) : new Color(255, 236, 210), null);
|
||||||
|
Style onlyRight = createStyle(textPane2, "onlyRight", dark ? new Color(45, 78, 55) : new Color(220, 255, 230), null);
|
||||||
|
Style selected1 = createStyle(textPane1, "selected1", dark ? new Color(45, 65, 105) : new Color(210, 230, 255), null);
|
||||||
|
Style selected2 = createStyle(textPane2, "selected2", dark ? new Color(45, 65, 105) : new Color(210, 230, 255), null);
|
||||||
|
|
||||||
|
for (int visibleRow = 0; visibleRow < visibleIndices.size(); visibleRow++) {
|
||||||
|
AlignedLine line = alignedLines.get(visibleIndices.get(visibleRow));
|
||||||
|
boolean selected = visibleRow == selectedVisibleRow
|
||||||
|
|| visibleRow == selectedLeftVisibleRow
|
||||||
|
|| visibleRow == selectedRightVisibleRow;
|
||||||
|
|
||||||
|
Style style1;
|
||||||
|
Style style2;
|
||||||
|
if (selected) {
|
||||||
|
style1 = selected1;
|
||||||
|
style2 = selected2;
|
||||||
|
} else if (!line.different) {
|
||||||
|
style1 = normal1;
|
||||||
|
style2 = normal2;
|
||||||
|
} else if (line.left == null) {
|
||||||
|
style1 = changed1;
|
||||||
|
style2 = onlyRight;
|
||||||
|
} else if (line.right == null) {
|
||||||
|
style1 = onlyLeft;
|
||||||
|
style2 = changed2;
|
||||||
|
} else {
|
||||||
|
style1 = changed1;
|
||||||
|
style2 = changed2;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc1.insertString(doc1.getLength(), (line.left != null ? line.left : "") + "\n", style1);
|
||||||
|
doc2.insertString(doc2.getLength(), (line.right != null ? line.right : "") + "\n", style2);
|
||||||
|
ln1.append(line.leftNumber > 0 ? line.leftNumber : "").append('\n');
|
||||||
|
ln2.append(line.rightNumber > 0 ? line.rightNumber : "").append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
lineNumbers1.setText(ln1.toString());
|
||||||
|
lineNumbers2.setText(ln2.toString());
|
||||||
|
|
||||||
|
if (selectedVisibleRow < 0 && !visibleIndices.isEmpty()) {
|
||||||
|
scrollToVisibleRow(0);
|
||||||
|
} else if (selectedVisibleRow >= 0) {
|
||||||
|
scrollToVisibleRow(selectedVisibleRow);
|
||||||
|
} else {
|
||||||
|
textPane1.setCaretPosition(0);
|
||||||
|
textPane2.setCaretPosition(0);
|
||||||
|
}
|
||||||
|
} catch (BadLocationException e) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Error updating display: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Style createStyle(JTextPane pane, String name, Color background, Color foreground) {
|
||||||
|
Style style = pane.addStyle(name, null);
|
||||||
|
if (background != null) {
|
||||||
|
StyleConstants.setBackground(style, background);
|
||||||
|
}
|
||||||
|
if (foreground != null) {
|
||||||
|
StyleConstants.setForeground(style, foreground);
|
||||||
|
} else {
|
||||||
|
StyleConstants.setForeground(style, pane.getForeground());
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void jumpToDifference(int direction) {
|
||||||
|
if (differenceVisibleRows.isEmpty()) {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedDifferencePointer < 0) {
|
||||||
|
selectedDifferencePointer = 0;
|
||||||
|
} else {
|
||||||
|
int size = differenceVisibleRows.size();
|
||||||
|
selectedDifferencePointer = (selectedDifferencePointer + direction + size) % size;
|
||||||
|
}
|
||||||
|
selectedVisibleRow = differenceVisibleRows.get(selectedDifferencePointer);
|
||||||
|
selectedLeftVisibleRow = selectedVisibleRow;
|
||||||
|
selectedRightVisibleRow = selectedVisibleRow;
|
||||||
|
render();
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectLineAtClick(JTextPane pane, Point point, boolean isLeftPane) {
|
||||||
|
int pos = pane.viewToModel2D(point);
|
||||||
|
if (pos < 0) return;
|
||||||
|
Element root = pane.getDocument().getDefaultRootElement();
|
||||||
|
int lineIndex = root.getElementIndex(pos);
|
||||||
|
if (lineIndex < 0 || lineIndex >= visibleIndices.size()) return;
|
||||||
|
|
||||||
|
selectedVisibleRow = lineIndex;
|
||||||
|
if (isLeftPane) {
|
||||||
|
selectedLeftVisibleRow = lineIndex;
|
||||||
|
} else {
|
||||||
|
selectedRightVisibleRow = lineIndex;
|
||||||
|
}
|
||||||
|
selectedDifferencePointer = -1;
|
||||||
|
for (int i = 0; i < differenceVisibleRows.size(); i++) {
|
||||||
|
if (differenceVisibleRows.get(i) == selectedVisibleRow) {
|
||||||
|
selectedDifferencePointer = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToVisibleRow(int row) {
|
||||||
|
if (row < 0) return;
|
||||||
|
try {
|
||||||
|
Element root1 = textPane1.getDocument().getDefaultRootElement();
|
||||||
|
Element root2 = textPane2.getDocument().getDefaultRootElement();
|
||||||
|
if (row >= root1.getElementCount() || row >= root2.getElementCount()) return;
|
||||||
|
|
||||||
|
Element line1 = root1.getElement(row);
|
||||||
|
Element line2 = root2.getElement(row);
|
||||||
|
int pos1 = line1.getStartOffset();
|
||||||
|
int pos2 = line2.getStartOffset();
|
||||||
|
|
||||||
|
textPane1.setCaretPosition(pos1);
|
||||||
|
textPane2.setCaretPosition(pos2);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus() {
|
||||||
|
int different = 0;
|
||||||
|
for (AlignedLine line : alignedLines) {
|
||||||
|
if (line.different) different++;
|
||||||
|
}
|
||||||
|
String alignMode = smartAlignCheck.isSelected() ? "smart" : "by position";
|
||||||
|
String visibleInfo = onlyDifferencesCheck.isSelected()
|
||||||
|
? "showing only differences"
|
||||||
|
: "showing all lines";
|
||||||
|
String syncInfo = (manualAnchorLeftLine > 0 && manualAnchorRightLine > 0)
|
||||||
|
? " | sync L" + manualAnchorLeftLine + " -> R" + manualAnchorRightLine
|
||||||
|
: "";
|
||||||
|
statusLabel.setText("Differences: " + different + " | " + visibleInfo + " | align: " + alignMode + syncInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyAppearance() {
|
private void applyAppearance() {
|
||||||
@ -213,28 +461,24 @@ public class FileComparisonDialog extends JFrame {
|
|||||||
textPane1.setBackground(bg);
|
textPane1.setBackground(bg);
|
||||||
textPane2.setBackground(bg);
|
textPane2.setBackground(bg);
|
||||||
boolean dark = isDark(bg);
|
boolean dark = isDark(bg);
|
||||||
textPane1.setForeground(dark ? Color.WHITE : Color.BLACK);
|
Color fg = dark ? Color.WHITE : Color.BLACK;
|
||||||
textPane2.setForeground(dark ? Color.WHITE : Color.BLACK);
|
textPane1.setForeground(fg);
|
||||||
textPane1.setCaretColor(dark ? Color.WHITE : Color.BLACK);
|
textPane2.setForeground(fg);
|
||||||
textPane2.setCaretColor(dark ? Color.WHITE : Color.BLACK);
|
textPane1.setCaretColor(fg);
|
||||||
|
textPane2.setCaretColor(fg);
|
||||||
|
|
||||||
if (lineNumbers1 != null) {
|
lineNumbers1.setBackground(dark ? bg.brighter() : bg.darker());
|
||||||
lineNumbers1.setBackground(dark ? bg.brighter() : bg.darker());
|
lineNumbers1.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY);
|
||||||
lineNumbers1.setForeground(dark ? Color.LIGHT_GRAY : Color.DARK_GRAY);
|
lineNumbers2.setBackground(dark ? bg.brighter() : bg.darker());
|
||||||
}
|
lineNumbers2.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();
|
Font f = config.getGlobalFont();
|
||||||
if (f != null) {
|
if (f != null) {
|
||||||
// Use monospaced variant of the font if possible, or just the same size
|
|
||||||
Font mono = new Font("Monospaced", Font.PLAIN, f.getSize());
|
Font mono = new Font("Monospaced", Font.PLAIN, f.getSize());
|
||||||
textPane1.setFont(mono);
|
textPane1.setFont(mono);
|
||||||
textPane2.setFont(mono);
|
textPane2.setFont(mono);
|
||||||
if (lineNumbers1 != null) lineNumbers1.setFont(mono);
|
lineNumbers1.setFont(mono);
|
||||||
if (lineNumbers2 != null) lineNumbers2.setFont(mono);
|
lineNumbers2.setFont(mono);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,128 +486,183 @@ public class FileComparisonDialog extends JFrame {
|
|||||||
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255 < 0.5;
|
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255 < 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDisplay() {
|
private List<AlignedLine> alignWithOptionalAnchor(List<String> left, List<String> right,
|
||||||
updateDisplay(true);
|
List<String> comparableLeft, List<String> comparableRight,
|
||||||
}
|
ComparisonOptions options) {
|
||||||
|
boolean validAnchor =
|
||||||
private void updateDisplay(boolean resetCaret) {
|
manualAnchorLeftLine > 0
|
||||||
try {
|
&& manualAnchorRightLine > 0
|
||||||
textPane1.setText("");
|
&& manualAnchorLeftLine <= left.size()
|
||||||
textPane2.setText("");
|
&& manualAnchorRightLine <= right.size();
|
||||||
StyledDocument doc1 = textPane1.getStyledDocument();
|
if (!validAnchor) {
|
||||||
StyledDocument doc2 = textPane2.getStyledDocument();
|
return options.smartAlign
|
||||||
StringBuilder ln1 = new StringBuilder();
|
? alignSmart(left, right, comparableLeft, comparableRight)
|
||||||
StringBuilder ln2 = new StringBuilder();
|
: alignByPosition(left, right, comparableLeft, comparableRight);
|
||||||
|
|
||||||
Style diffStyle = textPane1.addStyle("diff", null);
|
|
||||||
Color bg = textPane1.getBackground();
|
|
||||||
boolean dark = isDark(bg);
|
|
||||||
StyleConstants.setBackground(diffStyle, dark ? new Color(100, 30, 30) : new Color(255, 200, 200));
|
|
||||||
StyleConstants.setForeground(diffStyle, dark ? Color.WHITE : Color.BLACK);
|
|
||||||
|
|
||||||
int maxLines = Math.max(lines1.size(), lines2.size());
|
|
||||||
int counter1 = 1;
|
|
||||||
int counter2 = 1;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
textPane2.setCaretPosition(0);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
JOptionPane.showMessageDialog(this, "Error updating display: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int leftAnchorIndex = manualAnchorLeftLine - 1;
|
||||||
|
int rightAnchorIndex = manualAnchorRightLine - 1;
|
||||||
|
|
||||||
|
List<AlignedLine> result = new ArrayList<>();
|
||||||
|
List<AlignedLine> prefix = options.smartAlign
|
||||||
|
? alignSmartRange(left, right, comparableLeft, comparableRight, 0, leftAnchorIndex, 0, rightAnchorIndex)
|
||||||
|
: alignByPositionRange(left, right, comparableLeft, comparableRight, 0, leftAnchorIndex, 0, rightAnchorIndex);
|
||||||
|
result.addAll(prefix);
|
||||||
|
|
||||||
|
result.add(new AlignedLine(
|
||||||
|
left.get(leftAnchorIndex),
|
||||||
|
right.get(rightAnchorIndex),
|
||||||
|
manualAnchorLeftLine,
|
||||||
|
manualAnchorRightLine,
|
||||||
|
!equalsComparable(comparableLeft.get(leftAnchorIndex), comparableRight.get(rightAnchorIndex))
|
||||||
|
));
|
||||||
|
|
||||||
|
List<AlignedLine> suffix = options.smartAlign
|
||||||
|
? alignSmartRange(left, right, comparableLeft, comparableRight, leftAnchorIndex + 1, left.size(), rightAnchorIndex + 1, right.size())
|
||||||
|
: alignByPositionRange(left, right, comparableLeft, comparableRight, leftAnchorIndex + 1, left.size(), rightAnchorIndex + 1, right.size());
|
||||||
|
result.addAll(suffix);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performSmartAlignment() {
|
private List<AlignedLine> alignByPosition(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight) {
|
||||||
if (lines1.isEmpty() || lines2.isEmpty()) return;
|
return alignByPositionRange(left, right, comparableLeft, comparableRight, 0, left.size(), 0, right.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AlignedLine> alignByPositionRange(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight,
|
||||||
|
int leftStart, int leftEnd, int rightStart, int rightEnd) {
|
||||||
|
int leftLen = leftEnd - leftStart;
|
||||||
|
int rightLen = rightEnd - rightStart;
|
||||||
|
int max = Math.max(leftLen, rightLen);
|
||||||
|
List<AlignedLine> result = new ArrayList<>(max);
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
int li = leftStart + i;
|
||||||
|
int ri = rightStart + i;
|
||||||
|
String l = li < leftEnd ? left.get(li) : null;
|
||||||
|
String r = ri < rightEnd ? right.get(ri) : null;
|
||||||
|
String cl = li < leftEnd ? comparableLeft.get(li) : null;
|
||||||
|
String cr = ri < rightEnd ? comparableRight.get(ri) : null;
|
||||||
|
int leftNum = li < leftEnd ? li + 1 : -1;
|
||||||
|
int rightNum = ri < rightEnd ? ri + 1 : -1;
|
||||||
|
result.add(new AlignedLine(l, r, leftNum, rightNum, !equalsComparable(cl, cr)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AlignedLine> alignSmart(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight) {
|
||||||
|
return alignSmartRange(left, right, comparableLeft, comparableRight, 0, left.size(), 0, right.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AlignedLine> alignSmartRange(List<String> left, List<String> right, List<String> comparableLeft, List<String> comparableRight,
|
||||||
|
int leftStart, int leftEnd, int rightStart, int rightEnd) {
|
||||||
|
int n = leftEnd - leftStart;
|
||||||
|
int m = rightEnd - rightStart;
|
||||||
|
if (n == 0 && m == 0) return new ArrayList<>();
|
||||||
|
|
||||||
// Skip alignment for very large files to avoid O(N^2) memory/time issues
|
|
||||||
int maxLines = config.getMaxCompareLines();
|
int maxLines = config.getMaxCompareLines();
|
||||||
if (lines1.size() > maxLines || lines2.size() > maxLines) return;
|
if (n > maxLines || m > maxLines) {
|
||||||
|
return alignByPositionRange(left, right, comparableLeft, comparableRight, leftStart, leftEnd, rightStart, rightEnd);
|
||||||
|
}
|
||||||
|
|
||||||
int n = lines1.size();
|
int[][] lcs = new int[n + 1][m + 1];
|
||||||
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 i = 1; i <= n; i++) {
|
||||||
for (int j = 1; j <= m; j++) {
|
for (int j = 1; j <= m; j++) {
|
||||||
String s1 = lines1.get(i - 1);
|
if (equalsComparable(comparableLeft.get(leftStart + i - 1), comparableRight.get(rightStart + j - 1))) {
|
||||||
String s2 = lines2.get(j - 1);
|
lcs[i][j] = lcs[i - 1][j - 1] + 1;
|
||||||
if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) {
|
|
||||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
||||||
} else {
|
} else {
|
||||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> aligned1 = new ArrayList<>();
|
List<AlignedLine> reversed = new ArrayList<>();
|
||||||
List<String> aligned2 = new ArrayList<>();
|
int i = n;
|
||||||
int i = n, j = m;
|
int j = m;
|
||||||
while (i > 0 || j > 0) {
|
while (i > 0 || j > 0) {
|
||||||
if (i > 0 && j > 0) {
|
if (i > 0 && j > 0 && equalsComparable(comparableLeft.get(leftStart + i - 1), comparableRight.get(rightStart + j - 1))) {
|
||||||
String s1 = lines1.get(i - 1);
|
int li = leftStart + i - 1;
|
||||||
String s2 = lines2.get(j - 1);
|
int ri = rightStart + j - 1;
|
||||||
if (s1.equals(s2) || (!s1.trim().isEmpty() && s1.trim().equals(s2.trim()))) {
|
String l = left.get(li);
|
||||||
aligned1.add(0, s1);
|
String r = right.get(ri);
|
||||||
aligned2.add(0, s2);
|
reversed.add(new AlignedLine(l, r, li + 1, ri + 1, !equalsComparable(comparableLeft.get(li), comparableRight.get(ri))));
|
||||||
i--;
|
i--;
|
||||||
j--;
|
j--;
|
||||||
continue;
|
} else if (j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
||||||
}
|
int ri = rightStart + j - 1;
|
||||||
}
|
reversed.add(new AlignedLine(null, right.get(ri), -1, ri + 1, true));
|
||||||
|
|
||||||
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--;
|
j--;
|
||||||
} else if (i > 0) {
|
} else if (i > 0) {
|
||||||
aligned1.add(0, lines1.get(i - 1));
|
int li = leftStart + i - 1;
|
||||||
aligned2.add(0, null); // Added gap marker
|
reversed.add(new AlignedLine(left.get(li), null, li + 1, -1, true));
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lines1 = aligned1;
|
List<AlignedLine> result = new ArrayList<>(reversed.size());
|
||||||
this.lines2 = aligned2;
|
for (int idx = reversed.size() - 1; idx >= 0; idx--) {
|
||||||
|
result.add(reversed.get(idx));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> buildComparableLines(List<String> lines, ComparisonOptions options) {
|
||||||
|
List<String> comparable = new ArrayList<>(lines.size());
|
||||||
|
for (String line : lines) {
|
||||||
|
comparable.add(normalize(line, options));
|
||||||
|
}
|
||||||
|
return comparable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean equalsComparable(String left, String right) {
|
||||||
|
if (left == null || right == null) return left == right;
|
||||||
|
return left.equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String text, ComparisonOptions options) {
|
||||||
|
if (text == null) return "";
|
||||||
|
String normalized = text;
|
||||||
|
if (options.ignoreTrim) {
|
||||||
|
normalized = normalized.trim();
|
||||||
|
}
|
||||||
|
if (options.ignoreWhitespace) {
|
||||||
|
normalized = normalized.replaceAll("\\s+", "");
|
||||||
|
}
|
||||||
|
if (options.ignoreCase) {
|
||||||
|
normalized = normalized.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> readLines(File f) throws IOException {
|
private List<String> readLines(File f) throws IOException {
|
||||||
List<String> lines = new ArrayList<>();
|
try {
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
|
return Files.readAllLines(f.toPath(), StandardCharsets.UTF_8);
|
||||||
String line;
|
} catch (Exception e) {
|
||||||
while ((line = br.readLine()) != null) {
|
return Files.readAllLines(f.toPath());
|
||||||
lines.add(line);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class ComparisonOptions {
|
||||||
|
boolean smartAlign;
|
||||||
|
boolean ignoreCase;
|
||||||
|
boolean ignoreTrim;
|
||||||
|
boolean ignoreWhitespace;
|
||||||
|
boolean onlyDifferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AlignedLine {
|
||||||
|
final String left;
|
||||||
|
final String right;
|
||||||
|
final int leftNumber;
|
||||||
|
final int rightNumber;
|
||||||
|
final boolean different;
|
||||||
|
|
||||||
|
AlignedLine(String left, String right, int leftNumber, int rightNumber, boolean different) {
|
||||||
|
this.left = left;
|
||||||
|
this.right = right;
|
||||||
|
this.leftNumber = leftNumber;
|
||||||
|
this.rightNumber = rightNumber;
|
||||||
|
this.different = different;
|
||||||
}
|
}
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user