better compare files

This commit is contained in:
Radek Davidek 2026-04-29 19:55:22 +02:00
parent 351b59065e
commit aaf5ea7ce9
2 changed files with 81 additions and 94 deletions

View File

@ -14,9 +14,15 @@ import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@ -83,6 +89,26 @@ public class FileComparisonDialog extends JFrame {
dispose();
}
});
// TC-like quick navigation between differences.
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "nextDiff");
getRootPane().getActionMap().put("nextDiff", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
jumpToDifference(1);
}
});
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, KeyEvent.SHIFT_DOWN_MASK), "prevDiff");
getRootPane().getActionMap().put("prevDiff", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
jumpToDifference(-1);
}
});
if (!differenceVisibleRows.isEmpty()) {
jumpToDifference(1);
}
}
private void initComponents() {
@ -104,8 +130,6 @@ public class FileComparisonDialog extends JFrame {
JButton prevDiff = new JButton("Previous difference");
JButton nextDiff = new JButton("Next difference");
JButton synchronizeButton = new JButton("Synchronize from selected rows");
JButton clearSyncButton = new JButton("Clear sync");
JButton reloadButton = new JButton("Reload");
smartAlignCheck.addActionListener(e -> refreshComparison(false));
@ -117,10 +141,7 @@ public class FileComparisonDialog extends JFrame {
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(new JLabel("Compare:"));
panel.add(smartAlignCheck);
panel.add(ignoreCaseCheck);
panel.add(ignoreTrimCheck);
@ -128,8 +149,6 @@ public class FileComparisonDialog extends JFrame {
panel.add(onlyDifferencesCheck);
panel.add(prevDiff);
panel.add(nextDiff);
panel.add(synchronizeButton);
panel.add(clearSyncButton);
panel.add(reloadButton);
return panel;
}
@ -204,56 +223,6 @@ public class FileComparisonDialog extends JFrame {
refreshComparison(true);
}
private void synchronizeFromSelectedRows() {
if (selectedLeftVisibleRow < 0 || selectedRightVisibleRow < 0) {
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 refreshComparison(boolean resetSelection) {
ComparisonOptions options = getOptions();
List<String> comparableLeft = buildComparableLines(sourceLines1, options);
@ -445,6 +414,13 @@ public class FileComparisonDialog extends JFrame {
for (AlignedLine line : alignedLines) {
if (line.different) different++;
}
String diffPos;
if (differenceVisibleRows.isEmpty()) {
diffPos = "difference 0/0";
} else {
int pointer = selectedDifferencePointer >= 0 ? selectedDifferencePointer + 1 : 1;
diffPos = "difference " + pointer + "/" + differenceVisibleRows.size();
}
String alignMode = smartAlignCheck.isSelected() ? "smart" : "by position";
String visibleInfo = onlyDifferencesCheck.isSelected()
? "showing only differences"
@ -452,7 +428,7 @@ public class FileComparisonDialog extends JFrame {
String syncInfo = (manualAnchorLeftLine > 0 && manualAnchorRightLine > 0)
? " | sync L" + manualAnchorLeftLine + " -> R" + manualAnchorRightLine
: "";
statusLabel.setText("Differences: " + different + " | " + visibleInfo + " | align: " + alignMode + syncInfo);
statusLabel.setText("Differences: " + different + " | " + diffPos + " | " + visibleInfo + " | align: " + alignMode + syncInfo + " | F3/Shift+F3 next/prev");
}
private void applyAppearance() {
@ -635,11 +611,31 @@ public class FileComparisonDialog extends JFrame {
}
private List<String> readLines(File f) throws IOException {
try {
return Files.readAllLines(f.toPath(), StandardCharsets.UTF_8);
} catch (Exception e) {
return Files.readAllLines(f.toPath());
byte[] bytes = Files.readAllBytes(f.toPath());
if (bytes.length == 0) {
return List.of();
}
// Total Commander-like behavior: tolerate legacy encodings instead of failing.
String[] encodings = {"UTF-8", "windows-1250", "ISO-8859-1"};
String decoded = null;
for (String enc : encodings) {
try {
CharsetDecoder decoder = Charset.forName(enc).newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPORT);
decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
decoded = decoder.decode(ByteBuffer.wrap(bytes)).toString();
break;
} catch (CharacterCodingException ignored) {
// Try next encoding fallback.
}
}
if (decoded == null) {
decoded = new String(bytes, StandardCharsets.ISO_8859_1);
}
return Arrays.asList(decoded.split("\\R", -1));
}
private static class ComparisonOptions {
@ -665,4 +661,4 @@ public class FileComparisonDialog extends JFrame {
this.different = different;
}
}
}
}

View File

@ -3020,36 +3020,10 @@ public class MainWindow extends JFrame {
List<FileItem> leftSelection = leftPanel.getSelectedItems();
List<FileItem> rightSelection = rightPanel.getSelectedItems();
File leftFile = null;
File rightFile = null;
if (leftSelection.size() == 1) {
leftFile = leftSelection.getFirst().getFile();
} else if (leftSelection.size() > 1) {
JOptionPane.showMessageDialog(this, "Please select only one file in the left panel.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
if (rightSelection.size() == 1) {
rightFile = rightSelection.getFirst().getFile();
} else if (rightSelection.size() > 1) {
JOptionPane.showMessageDialog(this, "Please select only one file in the right panel.", "Compare Files", JOptionPane.WARNING_MESSAGE);
return;
}
// If nothing explicitly selected (marked) in a panel, use the focused item
if (leftFile == null) {
FileItem focused = leftPanel.getFocusedItem();
if (focused != null && !focused.getName().equals("..")) {
leftFile = focused.getFile();
}
}
if (rightFile == null) {
FileItem focused = rightPanel.getFocusedItem();
if (focused != null && !focused.getName().equals("..")) {
rightFile = focused.getFile();
}
}
// Total Commander-like behavior: compare focused file from each panel.
// If exactly one item is selected in a panel, use that one as an override.
File leftFile = getCompareCandidate(leftPanel, leftSelection);
File rightFile = getCompareCandidate(rightPanel, rightSelection);
if (leftFile == null || rightFile == null) {
JOptionPane.showMessageDialog(this, "Please select a file in both panels to compare.", "Compare Files", JOptionPane.WARNING_MESSAGE);
@ -3064,6 +3038,23 @@ public class MainWindow extends JFrame {
FileComparisonDialog dlg = new FileComparisonDialog(this, leftFile, rightFile, config);
dlg.setVisible(true);
}
private File getCompareCandidate(FilePanel panel, List<FileItem> selected) {
if (panel == null) return null;
if (selected != null && selected.size() == 1) {
FileItem item = selected.getFirst();
if (item != null && !"..".equals(item.getName())) {
return item.getFile();
}
}
FileItem focused = panel.getFocusedItem();
if (focused != null && !"..".equals(focused.getName())) {
return focused.getFile();
}
return null;
}
/**
* Refresh both panels while preserving selection and active panel focus.