charSpans, int charEventCount) {
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/enums/DiffTypeEnum.java b/src/main/java/com/luoboduner/moo/tool/enums/DiffTypeEnum.java
new file mode 100644
index 00000000..8d40a779
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/enums/DiffTypeEnum.java
@@ -0,0 +1,15 @@
+package com.luoboduner.moo.tool.enums;
+
+/**
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 18:42
+ */
+public enum DiffTypeEnum {
+
+ EQUAL, // 相同
+ INSERT, // 插入
+ DELETE, // 删除
+ CHANGE // 修改
+
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/enums/UnifiedSpanTypeEnum.java b/src/main/java/com/luoboduner/moo/tool/enums/UnifiedSpanTypeEnum.java
new file mode 100644
index 00000000..95e9121e
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/enums/UnifiedSpanTypeEnum.java
@@ -0,0 +1,18 @@
+package com.luoboduner.moo.tool.enums;
+
+/**
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 18:55
+ */
+public enum UnifiedSpanTypeEnum {
+
+ ADD_LINE,
+ DEL_LINE,
+ HUNK_LINE,
+ HEADER_LINE,
+ ADD_CHAR,
+ DEL_CHAR,
+ CHANGE_CHAR
+
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/service/DiffService.java b/src/main/java/com/luoboduner/moo/tool/service/DiffService.java
new file mode 100644
index 00000000..24fdfa09
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/service/DiffService.java
@@ -0,0 +1,344 @@
+package com.luoboduner.moo.tool.service;
+
+import com.github.difflib.DiffUtils;
+import com.github.difflib.UnifiedDiffUtils;
+import com.github.difflib.patch.AbstractDelta;
+import com.github.difflib.patch.DeltaType;
+import com.github.difflib.patch.Patch;
+import com.luoboduner.moo.tool.bean.textdiff.Span;
+import com.luoboduner.moo.tool.bean.textdiff.TextDiffSegment;
+import com.luoboduner.moo.tool.bean.textdiff.UIDiff;
+import com.luoboduner.moo.tool.bean.textdiff.UnifiedView;
+import com.luoboduner.moo.tool.enums.DiffTypeEnum;
+import com.luoboduner.moo.tool.enums.UnifiedSpanTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * 差异服务
+ * 计算 diff 并生成 unified diff(行级)
+ *
+ *
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 10:51
+ */
+@Slf4j
+public class DiffService {
+
+ /**
+ * 简单行切分器
+ *
+ * @param text 文本
+ * @return 行列表
+ */
+ private static List toLines(String text) {
+ // -1 保留末尾空格
+ return List.of(text.split("\\R", -1));
+ }
+
+ public static UIDiff getSegmentsForUI(String oldText, String newText, boolean ignoreWhitespace) {
+ return getSegmentsForUI(oldText, newText, ignoreWhitespace, false);
+ }
+
+ public static UIDiff getSegmentsForUI(String oldText, String newText, boolean ignoreWhitespace, boolean charOnly) {
+ List oldLines = toLines(oldText);
+ List newLines = toLines(newText);
+ Patch patch = DiffUtils.diff(oldLines, newLines);
+
+ // 计算每一行的起始字符偏移(含换行)
+ int[] leftLineStarts = computeLineStartOffsets(oldText);
+ int[] rightLineStarts = computeLineStartOffsets(newText);
+
+ List textDiffSegments = new ArrayList<>();
+
+ for (AbstractDelta delta : patch.getDeltas()) {
+ int leftPos = delta.getSource().getPosition();
+ int rightPos = delta.getTarget().getPosition();
+ List leftLines = delta.getSource().getLines();
+ List rightLines = delta.getTarget().getLines();
+ DeltaType type = delta.getType();
+
+ switch (type) {
+ case DELETE: {
+ if (charOnly) {
+ // 仅字符模式:整行删除不计入字符级差异
+ break;
+ }
+ for (int i = 0; i < leftLines.size(); i++) {
+ int lineIdx = leftPos + i;
+ int start = safeLineStart(leftLineStarts, lineIdx);
+ int end = start + leftLines.get(i).length();
+ if (!(ignoreWhitespace && isAllWhitespace(leftLines.get(i))))
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, start, end, -1, -1));
+ }
+ break;
+ }
+ case INSERT: {
+ if (charOnly) {
+ // 仅字符模式:整行插入不计入字符级差异
+ break;
+ }
+ for (int i = 0; i < rightLines.size(); i++) {
+ int lineIdx = rightPos + i;
+ int start = safeLineStart(rightLineStarts, lineIdx);
+ int end = start + rightLines.get(i).length();
+ if (!(ignoreWhitespace && isAllWhitespace(rightLines.get(i))))
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, start, end));
+ }
+ break;
+ }
+ case CHANGE: {
+ int pairCount = Math.min(leftLines.size(), rightLines.size());
+ // 对应行做字符级 diff
+ for (int i = 0; i < pairCount; i++) {
+ String a = leftLines.get(i);
+ String b = rightLines.get(i);
+ int aLineStart = safeLineStart(leftLineStarts, leftPos + i);
+ int bLineStart = safeLineStart(rightLineStarts, rightPos + i);
+ // 使用 DiffUtils 在字符层面做 diff
+ List ac = toChars(a);
+ List bc = toChars(b);
+ Patch charPatch = DiffUtils.diff(ac, bc);
+ for (AbstractDelta cDelta : charPatch.getDeltas()) {
+ int aStart = aLineStart + cDelta.getSource().getPosition();
+ int aEnd = aStart + cDelta.getSource().getLines().size();
+ int bStart = bLineStart + cDelta.getTarget().getPosition();
+ int bEnd = bStart + cDelta.getTarget().getLines().size();
+ if (cDelta.getType() == DeltaType.DELETE) {
+ if (!(ignoreWhitespace && isAllWhitespace(a.substring(
+ Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition())),
+ Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition() + cDelta.getSource().getLines().size()))
+ )))) {
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, aStart, aEnd, -1, -1));
+ }
+ } else if (cDelta.getType() == DeltaType.INSERT) {
+ if (!(ignoreWhitespace && isAllWhitespace(b.substring(
+ Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition())),
+ Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition() + cDelta.getTarget().getLines().size()))
+ )))) {
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, bStart, bEnd));
+ }
+ } else { // CHANGE
+ String aSub = a.substring(
+ Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition())),
+ Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition() + cDelta.getSource().getLines().size())));
+ String bSub = b.substring(
+ Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition())),
+ Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition() + cDelta.getTarget().getLines().size())));
+ if (!(ignoreWhitespace && equalsIgnoreWhitespace(aSub, bSub))) {
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.CHANGE, aStart, aEnd, bStart, bEnd));
+ }
+ }
+ }
+ }
+ if (!charOnly) {
+ // 左侧多余:视为整行删除
+ for (int i = pairCount; i < leftLines.size(); i++) {
+ int aLineStart = safeLineStart(leftLineStarts, leftPos + i);
+ if (!(ignoreWhitespace && isAllWhitespace(leftLines.get(i))))
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, aLineStart, aLineStart + leftLines.get(i).length(), -1, -1));
+ }
+ // 右侧多余:视为整行插入
+ for (int i = pairCount; i < rightLines.size(); i++) {
+ int bLineStart = safeLineStart(rightLineStarts, rightPos + i);
+ if (!(ignoreWhitespace && isAllWhitespace(rightLines.get(i))))
+ textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, bLineStart, bLineStart + rightLines.get(i).length()));
+ }
+ }
+ break;
+ }
+ case EQUAL:
+ // 不产生片段
+ break;
+ }
+ }
+
+ return new UIDiff(textDiffSegments);
+ }
+
+ private static int[] computeLineStartOffsets(String text) {
+ int n = 1; // 至少一行
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '\n') n++;
+ }
+ int[] starts = new int[n];
+ int idx = 0;
+ starts[idx++] = 0;
+ for (int i = 0; i < text.length(); i++) {
+ if (text.charAt(i) == '\n') {
+ if (idx < n) starts[idx++] = i + 1;
+ }
+ }
+ return starts;
+ }
+
+ private static int safeLineStart(int[] starts, int lineIndex) {
+ if (lineIndex < 0) {
+ return 0;
+ }
+ if (lineIndex >= starts.length) {
+ return (starts.length == 0 ? 0 : starts[starts.length - 1]);
+ }
+ return starts[lineIndex];
+ }
+
+ public static List toChars(String s) {
+ List list = new ArrayList<>(s.length());
+ for (int i = 0; i < s.length(); i++) {
+ list.add(String.valueOf(s.charAt(i)));
+ }
+ return list;
+ }
+
+ public static boolean isAllWhitespace(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean equalsIgnoreWhitespace(String a, String b) {
+ int i = 0, j = 0;
+ while (i < a.length() && j < b.length()) {
+ char ca = a.charAt(i);
+ char cb = b.charAt(j);
+ if (Character.isWhitespace(ca)) {
+ i++;
+ continue;
+ }
+ if (Character.isWhitespace(cb)) {
+ j++;
+ continue;
+ }
+ if (ca != cb) return false;
+ i++;
+ j++;
+ }
+ while (i < a.length()) {
+ if (!Character.isWhitespace(a.charAt(i++))) return false;
+ }
+ while (j < b.length()) {
+ if (!Character.isWhitespace(b.charAt(j++))) return false;
+ }
+ return true;
+ }
+
+ /**
+ * 构建统一差异视图(包含文本与高亮区间)
+ */
+ public static UnifiedView buildUnifiedView(String oldText, String newText, int context, boolean ignoreWhitespace) {
+ List oldLines = toLines(oldText);
+ List newLines = toLines(newText);
+ Patch patch = DiffUtils.diff(oldLines, newLines);
+ List unifiedLines = UnifiedDiffUtils.generateUnifiedDiff("old", "new", oldLines, patch, context);
+
+ String unifiedText = String.join("\n", unifiedLines);
+ List lineSpans = new ArrayList<>();
+ List charSpans = new ArrayList<>();
+ int charEvents = 0;
+
+ int offset = 0;
+ List minusStarts = new ArrayList<>();
+ List minusTexts = new ArrayList<>();
+ List plusStarts = new ArrayList<>();
+ List plusTexts = new ArrayList<>();
+
+ for (int i = 0; i < unifiedLines.size(); i++) {
+ String line = unifiedLines.get(i);
+ int lineStart = offset;
+ int lineLen = line.length();
+ int lineEnd = lineStart + lineLen;
+
+ if (line.startsWith("@@")) {
+ lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.HUNK_LINE));
+ charEvents += addIntralineSpans(minusStarts, minusTexts, plusStarts, plusTexts, charSpans, ignoreWhitespace);
+ minusStarts.clear();
+ minusTexts.clear();
+ plusStarts.clear();
+ plusTexts.clear();
+ } else if (line.startsWith("+")) {
+ if (!(ignoreWhitespace && isAllWhitespace(line.substring(1)))) {
+ lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.ADD_LINE));
+ }
+ plusStarts.add(lineStart);
+ plusTexts.add(line.substring(1));
+ } else if (line.startsWith("-")) {
+ if (!(ignoreWhitespace && isAllWhitespace(line.substring(1)))) {
+ lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.DEL_LINE));
+ }
+ minusStarts.add(lineStart);
+ minusTexts.add(line.substring(1));
+ } else if (line.startsWith("---") || line.startsWith("+++")) {
+ lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.HEADER_LINE));
+ }
+
+ offset = (i < unifiedLines.size() - 1) ? (lineEnd + 1) : lineEnd;
+ }
+
+ charEvents += addIntralineSpans(minusStarts, minusTexts, plusStarts, plusTexts, charSpans, ignoreWhitespace);
+
+ return new UnifiedView(unifiedText, lineSpans, charSpans, charEvents);
+ }
+
+ private static int addIntralineSpans(List minusStarts, List minusTexts,
+ List plusStarts, List plusTexts,
+ List out, boolean ignoreWhitespace) {
+ int events = 0;
+ int pairs = Math.min(minusTexts.size(), plusTexts.size());
+ for (int i = 0; i < pairs; i++) {
+ String a = minusTexts.get(i);
+ String b = plusTexts.get(i);
+ int aBase = minusStarts.get(i) + 1; // 跳过前缀符号
+ int bBase = plusStarts.get(i) + 1;
+ Patch charPatch = DiffUtils.diff(toChars(a), toChars(b));
+ for (AbstractDelta d : charPatch.getDeltas()) {
+ int aStart = aBase + d.getSource().getPosition();
+ int aEnd = aStart + d.getSource().getLines().size();
+ int bStart = bBase + d.getTarget().getPosition();
+ int bEnd = bStart + d.getTarget().getLines().size();
+ switch (d.getType()) {
+ case DELETE:
+ if (ignoreWhitespace && isAllWhitespace(safeSub(a, d.getSource().getPosition(), d.getSource().getLines().size())))
+ continue;
+ out.add(new Span(aStart, aEnd, UnifiedSpanTypeEnum.DEL_CHAR));
+ events++;
+ break;
+ case INSERT:
+ if (ignoreWhitespace && isAllWhitespace(safeSub(b, d.getTarget().getPosition(), d.getTarget().getLines().size())))
+ continue;
+ out.add(new Span(bStart, bEnd, UnifiedSpanTypeEnum.ADD_CHAR));
+ events++;
+ break;
+ case CHANGE:
+ String aSub = safeSub(a, d.getSource().getPosition(), d.getSource().getLines().size());
+ String bSub = safeSub(b, d.getTarget().getPosition(), d.getTarget().getLines().size());
+ if (ignoreWhitespace && equalsIgnoreWhitespace(aSub, bSub)) continue;
+ if (!aSub.isEmpty()) out.add(new Span(aStart, aEnd, UnifiedSpanTypeEnum.CHANGE_CHAR));
+ if (!bSub.isEmpty()) out.add(new Span(bStart, bEnd, UnifiedSpanTypeEnum.CHANGE_CHAR));
+ events++;
+ break;
+ case EQUAL:
+ break;
+ }
+ }
+ }
+ return events;
+ }
+
+ private static String safeSub(String s, int pos, int len) {
+ int start = Math.max(0, Math.min(pos, s.length()));
+ int end = Math.max(0, Math.min(pos + len, s.length()));
+ if (end < start) {
+ end = start;
+ }
+ return s.substring(start, end);
+ }
+
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/FuncConsts.java b/src/main/java/com/luoboduner/moo/tool/ui/FuncConsts.java
index 4a4ea98d..bb334281 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/FuncConsts.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/FuncConsts.java
@@ -26,4 +26,9 @@ public class FuncConsts {
* 二维码
*/
public static final String QR_CODE = "QrCode";
+
+ /**
+ * 文本对比
+ */
+ public static final String TEXT_DIFF = "TextDiff";
}
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/Init.java b/src/main/java/com/luoboduner/moo/tool/ui/Init.java
index bae6a3ea..0b0e5a19 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/Init.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/Init.java
@@ -213,6 +213,7 @@ public class Init {
ThreadUtil.execute(ImageForm::init);
ThreadUtil.execute(VariablesForm::init);
ThreadUtil.execute(YmlPropertiesForm::init);
+ ThreadUtil.execute(TextDiffForm::init);
ThreadUtil.execute(FileReformattingForm::init);
// 检查新版版
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.form b/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.form
index 5c157982..dfba9cf1 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.form
+++ b/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.form
@@ -210,6 +210,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.java b/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.java
index f580fb0f..4067fbae 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/form/MainWindow.java
@@ -47,11 +47,12 @@ public class MainWindow {
private JPanel pdfPanel;
private JPanel variablesPanel;
private JPanel ymlProperties;
+ private JPanel textDiffPanel;
private JPanel aboutPanel;
private static MainWindow mainWindow;
- private static final String[] ICON_PATH = {"icon/smile.svg", "icon/edit.svg", "icon/time.svg", "icon/json.svg", "icon/translate.svg", "icon/check.svg", "icon/global.svg", "icon/exchange.svg", "icon/QRcode.svg", "icon/method.svg", "icon/calculate.svg", "icon/network.svg", "icon/color.svg", "icon/image.svg", "icon/schedule.svg", "icon/reg.svg", "icon/java.svg", "icon/format_painter.svg", "icon/pdf.svg", "icon/fx.svg", "icon/suffix-yml.svg"};
+ private static final String[] ICON_PATH = {"icon/smile.svg", "icon/edit.svg", "icon/time.svg", "icon/json.svg", "icon/translate.svg", "icon/check.svg", "icon/global.svg", "icon/exchange.svg", "icon/QRcode.svg", "icon/method.svg", "icon/calculate.svg", "icon/network.svg", "icon/color.svg", "icon/image.svg", "icon/schedule.svg", "icon/reg.svg", "icon/java.svg", "icon/format_painter.svg", "icon/pdf.svg", "icon/fx.svg", "icon/suffix-yml.svg", "icon/find.svg"};
private MainWindow() {
}
@@ -96,6 +97,7 @@ public class MainWindow {
mainWindow.getPdfPanel().add(PdfForm.getInstance().getPdfPanel(), gridConstraints);
mainWindow.getVariablesPanel().add(VariablesForm.getInstance().getVariablesPanel(), gridConstraints);
mainWindow.getYmlProperties().add(YmlPropertiesForm.getInstance().getYmlPropertiesPanel(), gridConstraints);
+ mainWindow.getTextDiffPanel().add(TextDiffForm.getInstance().getTextDiffPanel(), gridConstraints);
mainWindow.getMainPanel().updateUI();
TabListener.addListeners();
@@ -285,6 +287,9 @@ public class MainWindow {
ymlProperties = new JPanel();
ymlProperties.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
tabbedPane.addTab("配置文件转换", ymlProperties);
+ textDiffPanel = new JPanel();
+ textDiffPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
+ tabbedPane.addTab("文本差异比对", textDiffPanel);
}
/**
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/form/func/TextDiffForm.java b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TextDiffForm.java
new file mode 100644
index 00000000..8f007578
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/ui/form/func/TextDiffForm.java
@@ -0,0 +1,966 @@
+package com.luoboduner.moo.tool.ui.form.func;
+
+import cn.hutool.log.Log;
+import cn.hutool.log.LogFactory;
+import com.formdev.flatlaf.extras.FlatSVGIcon;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import com.intellij.uiDesigner.core.Spacer;
+import com.luoboduner.moo.tool.bean.textdiff.UIDiff;
+import com.luoboduner.moo.tool.bean.textdiff.UnifiedView;
+import com.luoboduner.moo.tool.dao.TFuncContentMapper;
+import com.luoboduner.moo.tool.domain.TFuncContent;
+import com.luoboduner.moo.tool.service.DiffService;
+import com.luoboduner.moo.tool.ui.FuncConsts;
+import com.luoboduner.moo.tool.ui.listener.func.TextDiffListener;
+import com.luoboduner.moo.tool.util.MybatisUtil;
+import com.luoboduner.moo.tool.util.ScrollUtil;
+import com.luoboduner.moo.tool.util.SqliteUtil;
+import com.luoboduner.moo.tool.util.UndoUtil;
+import lombok.Getter;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.fife.ui.rtextarea.RTextScrollPane;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollBar;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.border.TitledBorder;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultHighlighter;
+import javax.swing.text.Highlighter;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ *
+ * TextDiffForm - 文本对比功能界面
+ *
+ *
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 10:51
+ */
+@Getter
+public class TextDiffForm {
+ private JPanel textDiffPanel;
+ private JPanel leftPanel;
+ private JPanel rightPanel;
+ private JPanel toolBarPanel;
+ private JButton compareButton;
+ private JButton clearButton;
+ private JButton swapButton;
+ private JButton copyDiffButton;
+ private JButton prevDiffButton;
+ private JButton nextDiffButton;
+ private JComboBox highlightModeComboBox;
+ private JLabel statusLabel;
+ private JCheckBox realTimeCheckBox;
+ private JCheckBox ignoreWhitespaceCheckBox;
+ private JComboBox displayModeComboBox;
+ private JSplitPane mainSplitPane;
+ private JScrollPane leftScrollPane;
+ private JScrollPane rightScrollPane;
+ private JScrollPane unifiedScrollPane;
+ private JPanel unifiedPanel;
+
+ private RSyntaxTextArea leftTextArea;
+ private RSyntaxTextArea rightTextArea;
+ private RSyntaxTextArea unifiedTextArea;
+
+ // Diff highlight painters
+ private final DefaultHighlighter.DefaultHighlightPainter delPainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 204, 204)); // light red for deletions
+ private final DefaultHighlighter.DefaultHighlightPainter insPainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(204, 255, 204)); // light green for insertions
+ private final DefaultHighlighter.DefaultHighlightPainter changePainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 249, 196)); // light yellow for modifications
+
+ // 滚动同步状态标记,避免循环触发
+ private boolean syncingScroll = false;
+
+ // 行级高亮(带透明度)
+ private final DefaultHighlighter.DefaultHighlightPainter delLinePainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 102, 102, 60));
+ private final DefaultHighlighter.DefaultHighlightPainter insLinePainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(0, 153, 0, 60));
+ private final DefaultHighlighter.DefaultHighlightPainter changeLinePainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 193, 7, 60));
+
+ // 统一视图:hunk 或标题行高亮
+ private final DefaultHighlighter.DefaultHighlightPainter headerLinePainter =
+ new DefaultHighlighter.DefaultHighlightPainter(new Color(230, 230, 230));
+
+ // 差异导航锚点与索引(按左/右侧段的中点行)
+ private final List leftNavOffsets = new ArrayList<>();
+ private final List rightNavOffsets = new ArrayList<>();
+ private int navIndex = -1;
+
+ private enum HighlightMode { CHAR_ONLY, LINE_ONLY, BOTH }
+ private HighlightMode highlightMode = HighlightMode.BOTH;
+
+ private static TextDiffForm textDiffForm;
+
+ private static final Log logger = LogFactory.get();
+
+ private static final TFuncContentMapper funcContentMapper = MybatisUtil.getSqlSession().getMapper(TFuncContentMapper.class);
+
+ private TextDiffForm() {
+ UndoUtil.register(this);
+ initTextAreas();
+ }
+
+ public static TextDiffForm getInstance() {
+ if (textDiffForm == null) {
+ textDiffForm = new TextDiffForm();
+ }
+ return textDiffForm;
+ }
+
+ public static void init() {
+ textDiffForm = getInstance();
+ textDiffForm.initForm();
+ }
+
+ /**
+ * 初始化文本区域
+ */
+ private void initTextAreas() {
+ // 左侧文本区域
+ leftTextArea = new RSyntaxTextArea();
+ leftTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
+ leftTextArea.setCodeFoldingEnabled(true);
+ leftTextArea.setAntiAliasingEnabled(true);
+ leftTextArea.setAutoIndentEnabled(true);
+ leftTextArea.setMarkOccurrences(true);
+ leftTextArea.setPaintMarkOccurrencesBorder(true);
+ leftTextArea.setTabSize(4);
+ leftTextArea.setCaretPosition(0);
+
+ // 右侧文本区域
+ rightTextArea = new RSyntaxTextArea();
+ rightTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
+ rightTextArea.setCodeFoldingEnabled(true);
+ rightTextArea.setAntiAliasingEnabled(true);
+ rightTextArea.setAutoIndentEnabled(true);
+ rightTextArea.setMarkOccurrences(true);
+ rightTextArea.setPaintMarkOccurrencesBorder(true);
+ rightTextArea.setTabSize(4);
+ rightTextArea.setCaretPosition(0);
+
+ // 统一diff显示区域
+ unifiedTextArea = new RSyntaxTextArea();
+ unifiedTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
+ unifiedTextArea.setCodeFoldingEnabled(true);
+ unifiedTextArea.setAntiAliasingEnabled(true);
+ unifiedTextArea.setEditable(false);
+ unifiedTextArea.setTabSize(4);
+ unifiedTextArea.setCaretPosition(0);
+
+ // 创建滚动面板
+ leftScrollPane = new RTextScrollPane(leftTextArea);
+ rightScrollPane = new RTextScrollPane(rightTextArea);
+ unifiedScrollPane = new RTextScrollPane(unifiedTextArea);
+
+ ScrollUtil.smoothPane(leftScrollPane);
+ ScrollUtil.smoothPane(rightScrollPane);
+ ScrollUtil.smoothPane(unifiedScrollPane);
+
+ // 同步左右滚动(便于并排对比时直观比照)
+ setupScrollSync();
+
+ // 将滚动面板添加到相应的面板中
+ if (leftPanel != null) {
+ leftPanel.add(
+ leftScrollPane,
+ new GridConstraints(
+ 0,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ null,
+ null,
+ null,
+ 0,
+ false
+ )
+ );
+ }
+ if (rightPanel != null) {
+ rightPanel.add(
+ rightScrollPane,
+ new GridConstraints(
+ 0,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ null,
+ null,
+ null,
+ 0,
+ false
+ )
+ );
+ }
+ if (unifiedPanel != null) {
+ unifiedPanel.add(unifiedScrollPane,
+ new GridConstraints(0,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ null,
+ null,
+ null,
+ 0,
+ false
+ )
+ );
+ }
+ }
+
+ /**
+ * 左右窗口竖向滚动同步
+ */
+ private void setupScrollSync() {
+ if (!(leftScrollPane instanceof RTextScrollPane) || !(rightScrollPane instanceof RTextScrollPane)) {
+ return;
+ }
+ // 获取滚动条
+ JScrollBar leftBar = leftScrollPane.getVerticalScrollBar();
+ JScrollBar rightBar = rightScrollPane.getVerticalScrollBar();
+ if (leftBar == null || rightBar == null) {
+ return;
+ }
+
+ // 滚动条滚动时,同步滚动
+ leftBar.addAdjustmentListener(e -> {
+ if (syncingScroll) return;
+ syncingScroll = true;
+ rightBar.setValue(e.getValue());
+ syncingScroll = false;
+ });
+ rightBar.addAdjustmentListener(e -> {
+ if (syncingScroll) return;
+ syncingScroll = true;
+ leftBar.setValue(e.getValue());
+ syncingScroll = false;
+ });
+ }
+
+ /**
+ * 初始化
+ */
+ public void initForm() {
+ TextDiffListener.addListeners();
+
+ // 设置默认显示模式
+ displayModeComboBox.addItem("并排对比");
+ displayModeComboBox.addItem("统一差异");
+ displayModeComboBox.setSelectedIndex(0);
+
+ // 高亮模式(默认仅字符,避免把相同部分也高亮)
+ highlightModeComboBox.addItem("双层高亮");
+ highlightModeComboBox.addItem("仅字符");
+ highlightModeComboBox.addItem("仅整行");
+ highlightModeComboBox.setSelectedIndex(1);
+ highlightModeComboBox.addActionListener(e -> {
+ switch ((String) Objects.requireNonNull(highlightModeComboBox.getSelectedItem())) {
+ case "仅字符":
+ highlightMode = HighlightMode.CHAR_ONLY; break;
+ case "仅整行":
+ highlightMode = HighlightMode.LINE_ONLY; break;
+ default:
+ highlightMode = HighlightMode.BOTH;
+ }
+ if (realTimeCheckBox.isSelected()) {
+ performDiff();
+ }
+ });
+
+ // 默认开启实时对比
+ realTimeCheckBox.setSelected(true);
+
+ // 初始化布局
+ updateDisplayMode();
+
+ // 恢复上次的内容
+ initContent();
+ }
+
+ /**
+ * 初始化内容
+ */
+ private void initContent() {
+ try {
+ TFuncContent tFuncContent = funcContentMapper.selectByFunc(FuncConsts.TEXT_DIFF + "_left");
+ if (tFuncContent != null) {
+ leftTextArea.setText(tFuncContent.getContent());
+ }
+
+ tFuncContent = funcContentMapper.selectByFunc(FuncConsts.TEXT_DIFF + "_right");
+ if (tFuncContent != null) {
+ rightTextArea.setText(tFuncContent.getContent());
+ }
+ } catch (Exception e) {
+ logger.error("初始化文本对比内容失败", e);
+ }
+ }
+
+ /**
+ * 保存内容
+ */
+ public void save() {
+ try {
+ saveLeftContent();
+ saveRightContent();
+ } catch (Exception e) {
+ logger.error("保存文本对比内容失败", e);
+ }
+ }
+
+ /**
+ * 保存左侧内容
+ */
+ private void saveLeftContent() {
+ String text = leftTextArea.getText();
+ String now = SqliteUtil.nowDateForSqlite();
+ String funcKey = FuncConsts.TEXT_DIFF + "_left";
+
+ saveContent(text, now, funcKey);
+ }
+
+ /**
+ * 保存内容
+ * @param text 文本
+ * @param now 时间
+ * @param funcKey 函数
+ */
+ private void saveContent(String text, String now, String funcKey) {
+ TFuncContent tFuncContent = funcContentMapper.selectByFunc(funcKey);
+ if (tFuncContent == null) {
+ tFuncContent = new TFuncContent();
+ tFuncContent.setFunc(funcKey);
+ tFuncContent.setContent(text);
+ tFuncContent.setCreateTime(now);
+ tFuncContent.setModifiedTime(now);
+ funcContentMapper.insert(tFuncContent);
+ } else {
+ tFuncContent.setContent(text);
+ tFuncContent.setModifiedTime(now);
+ funcContentMapper.updateByPrimaryKeySelective(tFuncContent);
+ }
+ }
+
+ /**
+ * 保存右侧内容
+ */
+ private void saveRightContent() {
+ String text = rightTextArea.getText();
+ String now = SqliteUtil.nowDateForSqlite();
+ String funcKey = FuncConsts.TEXT_DIFF + "_right";
+
+ saveContent(text, now, funcKey);
+ }
+
+ /**
+ * 更新显示模式
+ */
+ public void updateDisplayMode() {
+ String selectedMode = (String) displayModeComboBox.getSelectedItem();
+
+ if ("并排对比".equals(selectedMode)) {
+ // 显示并排对比模式
+ mainSplitPane.setLeftComponent(leftScrollPane);
+ mainSplitPane.setRightComponent(rightScrollPane);
+ mainSplitPane.setDividerLocation(0.5);
+ unifiedPanel.setVisible(false);
+ } else {
+ // 显示统一差异模式
+ mainSplitPane.setLeftComponent(leftScrollPane);
+ mainSplitPane.setRightComponent(rightScrollPane);
+ mainSplitPane.setDividerLocation(0.5);
+ unifiedPanel.setVisible(true);
+ }
+
+ textDiffPanel.revalidate();
+ textDiffPanel.repaint();
+ }
+
+ /**
+ * 执行文本对比
+ */
+ public void performDiff() {
+ String leftText = leftTextArea.getText();
+ String rightText = rightTextArea.getText();
+
+ if (leftText.isEmpty() && rightText.isEmpty()) {
+ statusLabel.setText("请输入要对比的文本");
+ return;
+ }
+
+ try {
+ // 统一差异视图(文本 + 高亮区间)
+ boolean ignoreWs = ignoreWhitespaceCheckBox != null && ignoreWhitespaceCheckBox.isSelected();
+ var unifiedView = DiffService.buildUnifiedView(leftText, rightText, 3, ignoreWs);
+ unifiedTextArea.setText(unifiedView.text());
+
+ // 获取用于UI高亮的差异信息(基于字符区间)
+ boolean charOnlyNav = (highlightMode == HighlightMode.CHAR_ONLY);
+ var diffResult = DiffService.getSegmentsForUI(leftText, rightText, ignoreWs, charOnlyNav);
+
+ // 构建导航锚点:每个差异段恰好追加一项(左/右没有则为 -1)
+ leftNavOffsets.clear();
+ rightNavOffsets.clear();
+ navIndex = -1;
+ if (diffResult.segments() != null) {
+ for (var seg : diffResult.segments()) {
+ int lMid = (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
+ ? (seg.leftStart() + seg.leftEnd()) / 2 : -1;
+ int rMid = (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
+ ? (seg.rightStart() + seg.rightEnd()) / 2 : -1;
+ leftNavOffsets.add(lMid);
+ rightNavOffsets.add(rMid);
+ }
+ }
+
+ // 左右并排的字符/行高亮
+ applyDiffHighlight(diffResult);
+ // 统一差异视图的行级/字符级高亮
+ applyUnifiedSpans(unifiedView);
+
+ // 更新状态:与可跳转点一致
+ int changes = (diffResult.segments() != null) ? diffResult.segments().size() : 0;
+ if (highlightMode == HighlightMode.CHAR_ONLY) {
+ statusLabel.setText("字符差异 " + changes + " 处");
+ } else {
+ statusLabel.setText("对比完成,共发现 " + changes + " 处差异");
+ }
+
+ } catch (Exception e) {
+ logger.error("执行文本对比失败", e);
+ statusLabel.setText("对比失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 清除两侧与统一视图上的所有高亮
+ */
+ private void clearHighlights() {
+ if (leftTextArea != null && leftTextArea.getHighlighter() != null) {
+ leftTextArea.getHighlighter().removeAllHighlights();
+ }
+ if (rightTextArea != null && rightTextArea.getHighlighter() != null) {
+ rightTextArea.getHighlighter().removeAllHighlights();
+ }
+ if (unifiedTextArea != null && unifiedTextArea.getHighlighter() != null) {
+ unifiedTextArea.getHighlighter().removeAllHighlights();
+ }
+ }
+
+ /**
+ * 应用差异高亮显示
+ */
+ private void applyDiffHighlight(UIDiff diffResult) {
+ clearHighlights();
+ if (diffResult == null || diffResult.segments() == null) {
+ return;
+ }
+
+ // 删除、插入、修改
+ for (var seg : diffResult.segments()) {
+ switch (seg.type()) {
+ case DELETE: {
+ if (highlightMode != HighlightMode.LINE_ONLY && seg.leftStart() >= 0 && seg.leftEnd() >= 0)
+ safeAddHighlight(leftTextArea, seg.leftStart(), seg.leftEnd(), delPainter);
+ if (highlightMode != HighlightMode.CHAR_ONLY && seg.leftStart() >= 0 && seg.leftEnd() >= 0)
+ safeAddLineHighlights(leftTextArea, seg.leftStart(), seg.leftEnd(), delLinePainter);
+ break;
+ }
+ case INSERT: {
+ if (highlightMode != HighlightMode.LINE_ONLY && seg.rightStart() >= 0 && seg.rightEnd() >= 0)
+ safeAddHighlight(rightTextArea, seg.rightStart(), seg.rightEnd(), insPainter);
+ if (highlightMode != HighlightMode.CHAR_ONLY && seg.rightStart() >= 0 && seg.rightEnd() >= 0)
+ safeAddLineHighlights(rightTextArea, seg.rightStart(), seg.rightEnd(), insLinePainter);
+ break;
+ }
+ case CHANGE: {
+ if (highlightMode != HighlightMode.LINE_ONLY) {
+ if (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
+ safeAddHighlight(leftTextArea, seg.leftStart(), seg.leftEnd(), changePainter);
+ if (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
+ safeAddHighlight(rightTextArea, seg.rightStart(), seg.rightEnd(), changePainter);
+ }
+ if (highlightMode != HighlightMode.CHAR_ONLY) {
+ if (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
+ safeAddLineHighlights(leftTextArea, seg.leftStart(), seg.leftEnd(), changeLinePainter);
+ if (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
+ safeAddLineHighlights(rightTextArea, seg.rightStart(), seg.rightEnd(), changeLinePainter);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * 在给定 TextArea 上安全添加高亮(自动裁剪区间并捕获异常)
+ */
+ private void safeAddHighlight(RSyntaxTextArea ta, int start, int end, Highlighter.HighlightPainter painter) {
+ if (ta == null || painter == null) {
+ return;
+ }
+ try {
+ int docLen = ta.getDocument().getLength();
+ int s = Math.max(0, Math.min(start, docLen));
+ int e = Math.max(0, Math.min(end, docLen));
+ if (e > s) {
+ ta.getHighlighter().addHighlight(s, e, painter);
+ }
+ } catch (BadLocationException ex) {
+ logger.warn("添加高亮失败: {}", ex.getMessage());
+ }
+ }
+
+ /**
+ * 以行为单位添加高亮:把 [start, end) 覆盖到完整的行区间并逐行高亮
+ */
+ private void safeAddLineHighlights(RSyntaxTextArea ta, int start, int end, Highlighter.HighlightPainter painter) {
+ if (ta == null || painter == null) return;
+ try {
+ int docLen = ta.getDocument().getLength();
+ int s = Math.max(0, Math.min(start, docLen));
+ int e = Math.max(0, Math.min(end, docLen));
+ if (e <= s) return;
+
+ int startLine = ta.getLineOfOffset(s);
+ int endLine = ta.getLineOfOffset(Math.max(s, e - 1));
+ for (int line = startLine; line <= endLine; line++) {
+ int ls = ta.getLineStartOffset(line);
+ int le = ta.getLineEndOffset(line);
+ // 为整行添加一层半透明底色,便于两侧比照
+ ta.getHighlighter().addHighlight(ls, le, painter);
+ }
+ } catch (BadLocationException ex) {
+ logger.warn("添加行级高亮失败: {}", ex.getMessage());
+ }
+ }
+
+ private void gotoDiff(int index) {
+ if (leftNavOffsets.isEmpty() && rightNavOffsets.isEmpty()) {
+ return;
+ }
+ int max = Math.max(leftNavOffsets.size(), rightNavOffsets.size());
+ navIndex = ((index % max) + max) % max; // 环绕
+ int lOff = (navIndex < leftNavOffsets.size()) ? leftNavOffsets.get(navIndex) : -1;
+ int rOff = (navIndex < rightNavOffsets.size()) ? rightNavOffsets.get(navIndex) : -1;
+ if (lOff >= 0) {
+ centerOnOffset(leftTextArea, lOff);
+ }
+ if (rOff >= 0) {
+ centerOnOffset(rightTextArea, rOff);
+ }
+ statusLabel.setText("跳至第 " + (navIndex + 1) + "/" + max + " 处差异");
+ }
+
+ private void centerOnOffset(RSyntaxTextArea ta, int offset) {
+ try {
+ int line = ta.getLineOfOffset(Math.max(0, Math.min(offset, ta.getDocument().getLength())));
+ int start = ta.getLineStartOffset(line);
+ ta.setCaretPosition(start);
+ ta.requestFocusInWindow();
+ } catch (BadLocationException ignore) { }
+ }
+
+ private void gotoNextDiff() { gotoDiff(navIndex + 1); }
+ private void gotoPrevDiff() { gotoDiff(navIndex - 1); }
+
+ /**
+ * 统一差异视图高亮:
+ * - 行级:+ 绿色, - 红色, @@ hunk 行黄色, 文件头灰色
+ * - 字符级:对同一 hunk 中的 +/- 行按序配对做字符级 diff,仅高亮变更范围
+ * 渲染统一差异视图的高亮(使用服务层返回的区间)
+ */
+ private void applyUnifiedSpans(UnifiedView view) {
+ if (view == null) return;
+ // 行级底色
+ if (view.lineSpans() != null) {
+ for (var sp : view.lineSpans()) {
+ switch (sp.type()) {
+ case ADD_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), insLinePainter);
+ case DEL_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), delLinePainter);
+ case HUNK_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), changeLinePainter);
+ case HEADER_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), headerLinePainter);
+ default -> {}
+ }
+ }
+ }
+ // 字符级高亮
+ if (view.charSpans() != null && highlightMode != HighlightMode.LINE_ONLY) {
+ for (var sp : view.charSpans()) {
+ switch (sp.type()) {
+ case ADD_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), insPainter);
+ case DEL_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), delPainter);
+ case CHANGE_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), changePainter);
+ default -> {}
+ }
+ }
+ }
+ }
+
+ /**
+ * 清空所有文本
+ */
+ public void clearAll() {
+ clearHighlights();
+ leftTextArea.setText("");
+ rightTextArea.setText("");
+ unifiedTextArea.setText("");
+ statusLabel.setText("已清空");
+ }
+
+ /**
+ * 交换左右文本
+ */
+ public void swapTexts() {
+ String leftText = leftTextArea.getText();
+ String rightText = rightTextArea.getText();
+
+ leftTextArea.setText(rightText);
+ rightTextArea.setText(leftText);
+
+ // 交换后也做一次对比,确保高亮与内容同步
+ performDiff();
+
+ statusLabel.setText("已交换文本");
+ }
+
+ /**
+ * 复制差异结果
+ */
+ public void copyDiffResult() {
+ String diffText = unifiedTextArea.getText();
+ if (!diffText.isEmpty()) {
+ Toolkit.getDefaultToolkit().getSystemClipboard()
+ .setContents(new java.awt.datatransfer.StringSelection(diffText), null);
+ statusLabel.setText("差异结果已复制到剪贴板");
+ } else {
+ statusLabel.setText("没有差异结果可复制");
+ }
+ }
+
+ {
+// GUI initializer generated by IntelliJ IDEA GUI Designer
+// >>> IMPORTANT!! <<<
+// DO NOT EDIT OR ADD ANY CODE HERE!
+ $$$setupUI$$$();
+ prevDiffButton.addActionListener(e -> gotoPrevDiff());
+ nextDiffButton.addActionListener(e -> gotoNextDiff());
+ }
+
+ /**
+ * Method generated by IntelliJ IDEA GUI Designer
+ * >>> IMPORTANT!! <<<
+ * DO NOT edit this method OR call it in your code!
+ *
+ * @noinspection ALL
+ */
+ private void $$$setupUI$$$() {
+ textDiffPanel = new JPanel();
+ textDiffPanel.setLayout(new GridLayoutManager(3, 1, new Insets(10, 10, 10, 10), -1, -1));
+
+ // 工具栏面板
+ toolBarPanel = new JPanel();
+ toolBarPanel.setLayout(new GridLayoutManager(1, 12, new Insets(0, 0, 10, 0), -1, -1));
+ textDiffPanel.add(toolBarPanel, new GridConstraints(0,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ compareButton = new JButton();
+ compareButton.setText("对比");
+ compareButton.setIcon(new FlatSVGIcon("icon/find.svg"));
+ toolBarPanel.add(compareButton, new GridConstraints(0,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ clearButton = new JButton();
+ clearButton.setText("清空");
+ clearButton.setIcon(new FlatSVGIcon("icon/remove.svg"));
+ toolBarPanel.add(clearButton, new GridConstraints(0,
+ 1,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ swapButton = new JButton();
+ swapButton.setText("交换");
+ swapButton.setIcon(new FlatSVGIcon("icon/exchange.svg"));
+ toolBarPanel.add(swapButton, new GridConstraints(0,
+ 2,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ copyDiffButton = new JButton();
+ copyDiffButton.setText("复制差异");
+ copyDiffButton.setIcon(new FlatSVGIcon("icon/copy.svg"));
+ toolBarPanel.add(copyDiffButton, new GridConstraints(0,
+ 3,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ prevDiffButton = new JButton();
+ prevDiffButton.setText("上一处");
+ toolBarPanel.add(prevDiffButton, new GridConstraints(0,
+ 4,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ nextDiffButton = new JButton();
+ nextDiffButton.setText("下一处");
+ toolBarPanel.add(nextDiffButton,
+ new GridConstraints(0,
+ 5,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ realTimeCheckBox = new JCheckBox();
+ realTimeCheckBox.setText("实时对比");
+ toolBarPanel.add(realTimeCheckBox, new GridConstraints(0,
+ 6,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_WEST,
+ GridConstraints.FILL_NONE,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ ignoreWhitespaceCheckBox = new JCheckBox();
+ ignoreWhitespaceCheckBox.setText("忽略空白");
+ toolBarPanel.add(ignoreWhitespaceCheckBox, new GridConstraints(0,
+ 7,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_WEST,
+ GridConstraints.FILL_NONE,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ highlightModeComboBox = new JComboBox<>();
+ toolBarPanel.add(highlightModeComboBox, new GridConstraints(0,
+ 8,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_WEST,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ displayModeComboBox = new JComboBox<>();
+ toolBarPanel.add(displayModeComboBox, new GridConstraints(0,
+ 9,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_WEST,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ final Spacer spacer1 = new Spacer();
+ toolBarPanel.add(spacer1, new GridConstraints(0,
+ 10,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_HORIZONTAL,
+ GridConstraints.SIZEPOLICY_WANT_GROW,
+ 1,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ statusLabel = new JLabel();
+ statusLabel.setText("准备就绪");
+ toolBarPanel.add(statusLabel, new GridConstraints(0,
+ 11,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_WEST,
+ GridConstraints.FILL_NONE,
+ GridConstraints.SIZEPOLICY_FIXED,
+ GridConstraints.SIZEPOLICY_FIXED,
+ null,
+ null,
+ null,
+ 0,
+ false));
+
+ // 主分割面板
+ mainSplitPane = new JSplitPane();
+ mainSplitPane.setDividerLocation(400);
+ mainSplitPane.setResizeWeight(0.5);
+ textDiffPanel.add(mainSplitPane, new GridConstraints(1,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ null,
+ new Dimension(200, 200),
+ null,
+ 0,
+ false));
+
+ // 左侧面板
+ leftPanel = new JPanel();
+ leftPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 5), -1, -1));
+ leftPanel.setBorder(BorderFactory.createTitledBorder(null, "原文本", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
+
+ // 右侧面板
+ rightPanel = new JPanel();
+ rightPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 5, 0, 0), -1, -1));
+ rightPanel.setBorder(BorderFactory.createTitledBorder(null, "新文本", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
+
+ // 设置分割面板的左右组件
+ mainSplitPane.setLeftComponent(leftPanel);
+ mainSplitPane.setRightComponent(rightPanel);
+
+ // 统一差异面板
+ unifiedPanel = new JPanel();
+ unifiedPanel.setLayout(new GridLayoutManager(1, 1, new Insets(10, 0, 0, 0), -1, -1));
+ unifiedPanel.setBorder(BorderFactory.createTitledBorder(null, "统一差异格式", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
+ unifiedPanel.setVisible(false);
+ textDiffPanel.add(unifiedPanel, new GridConstraints(2,
+ 0,
+ 1,
+ 1,
+ GridConstraints.ANCHOR_CENTER,
+ GridConstraints.FILL_BOTH,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+ null,
+ new Dimension(-1, 200),
+ null,
+ 0,
+ false));
+ }
+
+ /**
+ * @noinspection ALL
+ */
+ public JComponent $$$getRootComponent$$$() {
+ return textDiffPanel;
+ }
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/QuickNoteListener.java b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/QuickNoteListener.java
index 96e4016a..44fb863d 100644
--- a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/QuickNoteListener.java
+++ b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/QuickNoteListener.java
@@ -1094,76 +1094,94 @@ public class QuickNoteListener {
public static void format() {
try {
QuickNoteForm quickNoteForm = QuickNoteForm.getInstance();
- String text = QuickNoteForm.quickNoteRSyntaxTextViewerManager.getTextByName(selectedName);
-
- String format;
- String selectedSyntax = (String) quickNoteForm.getSyntaxComboBox().getSelectedItem();
+ final String text = QuickNoteForm.quickNoteRSyntaxTextViewerManager.getTextByName(selectedName);
+ final String selectedSyntax = (String) quickNoteForm.getSyntaxComboBox().getSelectedItem();
if (StringUtils.isBlank(text) || StringUtils.isEmpty(selectedSyntax)) {
return;
}
- switch ("text/" + selectedSyntax) {
- case SyntaxConstants.SYNTAX_STYLE_SQL:
- switch (App.config.getSqlDialect()) {
- case "MariaDB":
- format = SqlFormatter.of(Dialect.MariaDb).format(text, " ");
+ executorService.submit(() -> {
+ try {
+ String formatted;
+ switch ("text/" + selectedSyntax) {
+ case SyntaxConstants.SYNTAX_STYLE_SQL:
+ switch (App.config.getSqlDialect()) {
+ case "MariaDB":
+ formatted = SqlFormatter.of(Dialect.MariaDb).format(text, " ");
+ break;
+ case "MySQL":
+ formatted = SqlFormatter.of(Dialect.MySql).format(text, " ");
+ break;
+ case "PostgreSQL":
+ formatted = SqlFormatter.of(Dialect.PostgreSql).format(text, " ");
+ break;
+ case "IBM DB2":
+ formatted = SqlFormatter.of(Dialect.Db2).format(text, " ");
+ break;
+ case "Oracle PL/SQL":
+ formatted = SqlFormatter.of(Dialect.PlSql).format(text, " ");
+ break;
+ case "Couchbase N1QL":
+ formatted = SqlFormatter.of(Dialect.N1ql).format(text, " ");
+ break;
+ case "Amazon Redshift":
+ formatted = SqlFormatter.of(Dialect.Redshift).format(text, " ");
+ break;
+ case "Spark":
+ formatted = SqlFormatter.of(Dialect.SparkSql).format(text, " ");
+ break;
+ case "SQL Server Transact-SQL":
+ formatted = SqlFormatter.of(Dialect.TSql).format(text, " ");
+ break;
+ default:
+ formatted = SqlFormatter.of(Dialect.StandardSql).format(text, " ");
+ }
break;
- case "MySQL":
- format = SqlFormatter.of(Dialect.MySql).format(text, " ");
+ case SyntaxConstants.SYNTAX_STYLE_JSON:
+ case SyntaxConstants.SYNTAX_STYLE_JSON_WITH_COMMENTS:
+ try {
+ formatted = JSONUtil.toJsonPrettyStr(text);
+ } catch (Exception e1) {
+ log.error(ExceptionUtils.getStackTrace(e1));
+ formatted = JSONUtil.formatJsonStr(text);
+ }
break;
- case "PostgreSQL":
- format = SqlFormatter.of(Dialect.PostgreSql).format(text, " ");
+
+ case SyntaxConstants.SYNTAX_STYLE_XML:
+ formatted = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.XML).format(text);
break;
- case "IBM DB2":
- format = SqlFormatter.of(Dialect.Db2).format(text, " ");
- break;
- case "Oracle PL/SQL":
- format = SqlFormatter.of(Dialect.PlSql).format(text, " ");
- break;
- case "Couchbase N1QL":
- format = SqlFormatter.of(Dialect.N1ql).format(text, " ");
- break;
- case "Amazon Redshift":
- format = SqlFormatter.of(Dialect.Redshift).format(text, " ");
- break;
- case "Spark":
- format = SqlFormatter.of(Dialect.SparkSql).format(text, " ");
- break;
- case "SQL Server Transact-SQL":
- format = SqlFormatter.of(Dialect.TSql).format(text, " ");
+
+ case SyntaxConstants.SYNTAX_STYLE_JAVA:
+ formatted = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.JAVA).format(text);
break;
default:
- format = SqlFormatter.of(Dialect.StandardSql).format(text, " ");
+ SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(App.mainFrame, "尚不支持对该语言格式化!\n", "不支持该语言",
+ JOptionPane.INFORMATION_MESSAGE));
+ return;
}
- break;
- case SyntaxConstants.SYNTAX_STYLE_JSON:
- case SyntaxConstants.SYNTAX_STYLE_JSON_WITH_COMMENTS:
- try {
- format = JSONUtil.toJsonPrettyStr(text);
- } catch (Exception e1) {
- log.error(ExceptionUtils.getStackTrace(e1));
- format = JSONUtil.formatJsonStr(text);
- }
- break;
- case SyntaxConstants.SYNTAX_STYLE_JAVA:
- format = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.JAVA).format(text);
- break;
- default:
- JOptionPane.showMessageDialog(App.mainFrame, "尚不支持对该语言格式化!\n", "不支持该语言",
- JOptionPane.INFORMATION_MESSAGE);
- return;
- }
-
- QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setText(format);
- QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setCaretPosition(0);
-
- QuickNoteIndicatorTools.showTips("已格式化:" + selectedName, QuickNoteIndicatorTools.TipsLevel.SUCCESS);
+ final String result = formatted;
+ SwingUtilities.invokeLater(() -> {
+ try {
+ QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setText(result);
+ QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setCaretPosition(0);
+ QuickNoteIndicatorTools.showTips("已格式化:" + selectedName, QuickNoteIndicatorTools.TipsLevel.SUCCESS);
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + ex.getMessage(), "失败",
+ JOptionPane.ERROR_MESSAGE);
+ log.error(ExceptionUtils.getStackTrace(ex));
+ }
+ });
+ } catch (Exception e) {
+ SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + e.getMessage(), "失败",
+ JOptionPane.ERROR_MESSAGE));
+ log.error(ExceptionUtils.getStackTrace(e));
+ }
+ });
} catch (Exception e) {
JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + e.getMessage(), "失败",
JOptionPane.ERROR_MESSAGE);
log.error(ExceptionUtils.getStackTrace(e));
}
-
}
}
diff --git a/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TextDiffListener.java b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TextDiffListener.java
new file mode 100644
index 00000000..3bbc2cce
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/ui/listener/func/TextDiffListener.java
@@ -0,0 +1,133 @@
+package com.luoboduner.moo.tool.ui.listener.func;
+
+import com.luoboduner.moo.tool.ui.form.func.TextDiffForm;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.swing.SwingUtilities;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+/**
+ *
+ * TextDiffListener - 文本对比功能事件监听器
+ *
+ *
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 10:51
+ */
+@Slf4j
+public class TextDiffListener {
+
+ /**
+ * 添加事件监听器
+ */
+ public static void addListeners() {
+ TextDiffForm textDiffForm = TextDiffForm.getInstance();
+
+ // 对比按钮事件
+ textDiffForm.getCompareButton().addActionListener(e -> {
+ textDiffForm.performDiff();
+ });
+
+ // 清空按钮事件
+ textDiffForm.getClearButton().addActionListener(e -> {
+ textDiffForm.clearAll();
+ });
+
+ // 交换按钮事件
+ textDiffForm.getSwapButton().addActionListener(e -> {
+ textDiffForm.swapTexts();
+ });
+
+ // 复制差异按钮事件
+ textDiffForm.getCopyDiffButton().addActionListener(e -> {
+ textDiffForm.copyDiffResult();
+ });
+
+ // 显示模式切换事件
+ textDiffForm.getDisplayModeComboBox().addActionListener(e -> {
+ textDiffForm.updateDisplayMode();
+ // 如果当前有内容,重新执行对比
+ if (!textDiffForm.getLeftTextArea().getText().isEmpty() ||
+ !textDiffForm.getRightTextArea().getText().isEmpty()) {
+ textDiffForm.performDiff();
+ }
+ });
+
+ // 实时对比复选框事件
+ textDiffForm.getRealTimeCheckBox().addActionListener(e -> {
+ if (textDiffForm.getRealTimeCheckBox().isSelected()) {
+ // 开启实时对比时立即执行一次对比
+ textDiffForm.performDiff();
+ }
+ });
+
+ // 忽略空白差异复选框事件:切换时立即重算
+ textDiffForm.getIgnoreWhitespaceCheckBox().addActionListener(e -> {
+ textDiffForm.performDiff();
+ });
+
+ // 文本变化监听器 - 实时对比
+ DocumentListener realTimeDocumentListener = new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ if (textDiffForm.getRealTimeCheckBox().isSelected()) {
+ // 延迟执行,避免频繁对比
+ javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
+ }
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ if (textDiffForm.getRealTimeCheckBox().isSelected()) {
+ javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
+ }
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ if (textDiffForm.getRealTimeCheckBox().isSelected()) {
+ javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
+ }
+ }
+ };
+
+ // 为左右文本区域添加文档监听器
+ textDiffForm.getLeftTextArea().getDocument().addDocumentListener(realTimeDocumentListener);
+ textDiffForm.getRightTextArea().getDocument().addDocumentListener(realTimeDocumentListener);
+
+ // 内容保存监听器
+ DocumentListener saveDocumentListener = new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ saveContent();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ saveContent();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ saveContent();
+ }
+
+ private void saveContent() {
+ // 延迟保存,避免频繁IO操作
+ SwingUtilities.invokeLater(() -> {
+ try {
+ textDiffForm.save();
+ } catch (Exception ex) {
+ log.error("保存文本对比内容失败", ex);
+ }
+ });
+ }
+ };
+
+ // 为左右文本区域添加保存监听器
+ textDiffForm.getLeftTextArea().getDocument().addDocumentListener(saveDocumentListener);
+ textDiffForm.getRightTextArea().getDocument().addDocumentListener(saveDocumentListener);
+ }
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/util/FileReformatUtil.java b/src/main/java/com/luoboduner/moo/tool/util/FileReformatUtil.java
index 5f8f8b0d..6b3c1c49 100755
--- a/src/main/java/com/luoboduner/moo/tool/util/FileReformatUtil.java
+++ b/src/main/java/com/luoboduner/moo/tool/util/FileReformatUtil.java
@@ -1,92 +1,45 @@
-package com.luoboduner.moo.tool.util;
-
-import de.hunsicker.jalopy.Jalopy;
-import de.hunsicker.jalopy.storage.Convention;
-import de.hunsicker.jalopy.storage.ConventionKeys;
-import org.jsoup.Jsoup;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import java.io.File;
-import java.io.StringWriter;
-
-public class FileReformatUtil {
-
- private static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
- private static Jalopy jalopy = new Jalopy();
-
- public static String reformatXML(File file, int spaceNum) throws Exception {
- StringWriter res = new StringWriter();
- try {
- // 创建DocumentBuilder对象
- DocumentBuilder builder = factory.newDocumentBuilder();
-
- // 解析XML文件并返回Document对象
- Document document = builder.parse(file);
- // 获取文档的根元素
- Element rootElement = document.getDocumentElement();
-
- // 设置格式化属性,这里使用缩进和换行
- document.setXmlStandalone(true);
- document.setXmlVersion("1.0");
- rootElement.normalize();
-
- // 创建Transformer对象
- Transformer transformer = transformerFactory.newTransformer();
-
- // 设置输出格式化属性,这里使用缩进和换行
- transformer.setOutputProperty(OutputKeys.INDENT, "yes");
- transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
- transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(spaceNum));
-
- // 创建DOMSource对象
- DOMSource source = new DOMSource(document);
-
- // 创建StreamResult对象,指定输出位置
- StreamResult result = new StreamResult(res);
-
- // 格式化XML并输出结果
- transformer.transform(source, result);
-
- return res.toString();
- } catch (Exception e) {
- throw e;
- } finally {
- res.close();
- }
- }
-
- public static String reformatHTML(File file, int spaceNum) throws Exception {
- return Jsoup.parse(file, "UTF-8")
- .outputSettings(new org.jsoup.nodes.Document.OutputSettings().indentAmount(spaceNum))
- .outerHtml();
- }
-
- public static String reformatJAVA(File file, int spaceNum) throws Exception {
- Convention instance = Convention.getInstance();
- instance.put(ConventionKeys.SPACE_BEFORE_BRACES, "true");
- instance.put(ConventionKeys.SPACE_BEFORE_BRACKETS, "true");
- instance.put(ConventionKeys.INDENT_SIZE, String.valueOf(spaceNum));
-
- try (StringWriter stringWriter = new StringWriter()) {
- jalopy.setInput(file);
- jalopy.setOutput(stringWriter);
- boolean result = jalopy.format();
- if (!result) {
- throw new Exception("格式化失败!");
- }
-
- return stringWriter.toString();
- } catch (Exception e) {
- throw e;
- }
- }
-}
+package com.luoboduner.moo.tool.util;
+
+import com.luoboduner.moo.tool.util.codeformatter.XmlFormatting;
+import de.hunsicker.jalopy.Jalopy;
+import de.hunsicker.jalopy.storage.Convention;
+import de.hunsicker.jalopy.storage.ConventionKeys;
+import org.jsoup.Jsoup;
+
+import java.io.File;
+import java.io.StringWriter;
+
+public class FileReformatUtil {
+
+ private static Jalopy jalopy = new Jalopy();
+
+ public static String reformatXML(File file, int spaceNum) throws Exception {
+ return XmlFormatting.formatFile(file, spaceNum);
+ }
+
+ public static String reformatHTML(File file, int spaceNum) throws Exception {
+ return Jsoup.parse(file, "UTF-8")
+ .outputSettings(new org.jsoup.nodes.Document.OutputSettings().indentAmount(spaceNum))
+ .outerHtml();
+ }
+
+ public static String reformatJAVA(File file, int spaceNum) throws Exception {
+ Convention instance = Convention.getInstance();
+ instance.put(ConventionKeys.SPACE_BEFORE_BRACES, "true");
+ instance.put(ConventionKeys.SPACE_BEFORE_BRACKETS, "true");
+ instance.put(ConventionKeys.INDENT_SIZE, String.valueOf(spaceNum));
+
+ try (StringWriter stringWriter = new StringWriter()) {
+ jalopy.setInput(file);
+ jalopy.setOutput(stringWriter);
+ boolean result = jalopy.format();
+ if (!result) {
+ throw new Exception("格式化失败!");
+ }
+
+ return stringWriter.toString();
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/util/XmlReformatUtil.java b/src/main/java/com/luoboduner/moo/tool/util/XmlReformatUtil.java
index d2b9f37a..60df2671 100755
--- a/src/main/java/com/luoboduner/moo/tool/util/XmlReformatUtil.java
+++ b/src/main/java/com/luoboduner/moo/tool/util/XmlReformatUtil.java
@@ -1,90 +1,25 @@
-package com.luoboduner.moo.tool.util;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.xml.sax.InputSource;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.StringWriter;
-
-public class XmlReformatUtil {
- // log
- private static final Logger logger = LoggerFactory.getLogger(XmlReformatUtil.class);
-
- private static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
-
- public static String reformat(File file, int spaceNum) throws Exception {
- StringWriter res = new StringWriter();
- try {
- // 创建DocumentBuilder对象
- DocumentBuilder builder = factory.newDocumentBuilder();
-
- // 解析XML文件并返回Document对象
- Document document = builder.parse(file);
- // 获取文档的根元素
- Element rootElement = document.getDocumentElement();
-
- // 设置格式化属性,这里使用缩进和换行
- document.setXmlStandalone(true);
- document.setXmlVersion("1.0");
- rootElement.normalize();
-
- // 创建Transformer对象
- Transformer transformer = transformerFactory.newTransformer();
-
- // 设置输出格式化属性,这里使用缩进和换行
- transformer.setOutputProperty(OutputKeys.INDENT, "yes");
- transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
- transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(spaceNum));
-
- // 创建DOMSource对象
- DOMSource source = new DOMSource(document);
-
- // 创建StreamResult对象,指定输出位置
- StreamResult result = new StreamResult(res);
-
- // 格式化XML并输出结果
- transformer.transform(source, result);
-
- return res.toString();
- } catch (Exception e) {
- throw e;
- } finally {
- res.close();
- }
- }
-
- public static String format(String xmlStr) {
- try {
- DocumentBuilder builder = factory.newDocumentBuilder();
- Document document = builder.parse(new InputSource(new ByteArrayInputStream(xmlStr.getBytes("utf-8"))));
- Element rootElement = document.getDocumentElement();
- document.setXmlStandalone(true);
- document.setXmlVersion("1.0");
- rootElement.normalize();
- Transformer transformer = transformerFactory.newTransformer();
- transformer.setOutputProperty(OutputKeys.INDENT, "yes");
- transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
- transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
- DOMSource source = new DOMSource(document);
- StringWriter res = new StringWriter();
- StreamResult result = new StreamResult(res);
- transformer.transform(source, result);
- return res.toString();
- } catch (Exception e) {
- logger.error("XML格式化失败", e);
- return xmlStr;
- }
- }
-}
+package com.luoboduner.moo.tool.util;
+
+import com.luoboduner.moo.tool.util.codeformatter.XmlFormatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+public class XmlReformatUtil {
+ // log
+ private static final Logger logger = LoggerFactory.getLogger(XmlReformatUtil.class);
+
+ public static String reformat(File file, int spaceNum) throws Exception {
+ return XmlFormatting.formatFile(file, spaceNum);
+ }
+
+ public static String format(String xmlStr) {
+ try {
+ return XmlFormatting.formatString(xmlStr, 4);
+ } catch (Exception e) {
+ logger.error("XML格式化失败", e);
+ return xmlStr;
+ }
+ }
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatter.java b/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatter.java
index 0a7cccb6..86a57cc1 100644
--- a/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatter.java
+++ b/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatter.java
@@ -1,38 +1,14 @@
package com.luoboduner.moo.tool.util.codeformatter;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import java.io.StringReader;
-import java.io.StringWriter;
-
public class XmlFormatter implements CodeFormatter {
public String format(String input) {
try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- DocumentBuilder builder = factory.newDocumentBuilder();
- Document document = builder.parse(new InputSource(new StringReader(input)));
-
- TransformerFactory transformerFactory = TransformerFactory.newInstance();
- Transformer transformer = transformerFactory.newTransformer();
- transformer.setOutputProperty(OutputKeys.INDENT, "yes");
- transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
-
- StringWriter writer = new StringWriter();
- transformer.transform(new DOMSource(document), new StreamResult(writer));
- return writer.toString();
+ // Delegate to centralized safe XML formatter with default indent 4
+ return XmlFormatting.formatString(input, 4);
} catch (Exception e) {
throw new RuntimeException("Error formatting XML", e);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatting.java b/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatting.java
new file mode 100644
index 00000000..6206ef8f
--- /dev/null
+++ b/src/main/java/com/luoboduner/moo/tool/util/codeformatter/XmlFormatting.java
@@ -0,0 +1,137 @@
+package com.luoboduner.moo.tool.util.codeformatter;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.parser.Parser;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.StringWriter;
+
+/**
+ * XML格式化工具
+ *
+ * @author CassianFlorin
+ * @email flowercard591@gmail.com
+ * @date 2025/9/27 22:15
+ */
+public final class XmlFormatting {
+
+ private XmlFormatting() {
+ }
+
+ public static String formatString(String xml, int indent) throws Exception {
+ try {
+ return formatWithJsoup(xml, indent);
+ } catch (Exception ignore) {
+ // fallback to JAXP secure DOM formatting
+ org.w3c.dom.Document document = parseString(xml);
+ return transform(document, indent);
+ }
+ }
+
+ public static String formatFile(File file, int indent) throws Exception {
+ try {
+ String content = readFileUtf8(file);
+ return formatWithJsoup(content, indent);
+ } catch (Exception ignore) {
+ org.w3c.dom.Document document = parseFile(file);
+ return transform(document, indent);
+ }
+ }
+
+ private static String formatWithJsoup(String xml, int indent) throws Exception {
+ Document doc = Jsoup.parse(xml, "", Parser.xmlParser());
+ doc.outputSettings(new Document.OutputSettings()
+ .prettyPrint(true)
+ .indentAmount(indent)
+ .syntax(Document.OutputSettings.Syntax.xml));
+ return doc.outerHtml();
+ }
+
+ private static org.w3c.dom.Document parseString(String xml) throws Exception {
+ DocumentBuilderFactory factory = secureDocumentBuilderFactory();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ InputSource source = new InputSource(new ByteArrayInputStream(xml.getBytes("UTF-8")));
+ org.w3c.dom.Document document = builder.parse(source);
+ normalize(document);
+ return document;
+ }
+
+ private static org.w3c.dom.Document parseFile(File file) throws Exception {
+ DocumentBuilderFactory factory = secureDocumentBuilderFactory();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ org.w3c.dom.Document document = builder.parse(file);
+ normalize(document);
+ return document;
+ }
+
+ private static void normalize(org.w3c.dom.Document document) {
+ Element rootElement = document.getDocumentElement();
+ document.setXmlStandalone(true);
+ document.setXmlVersion("1.0");
+ if (rootElement != null) {
+ rootElement.normalize();
+ }
+ }
+
+ private static String transform(org.w3c.dom.Document document, int indent) throws Exception {
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ try {
+ transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ } catch (Throwable ignore) {
+ // Some TransformerFactory implementations may not support this feature; ignore to keep compatibility
+ }
+ Transformer transformer = transformerFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent));
+
+ StringWriter res = new StringWriter();
+ transformer.transform(new DOMSource(document), new StreamResult(res));
+ return res.toString();
+ }
+
+ private static String readFileUtf8(File file) throws Exception {
+ byte[] bytes = java.nio.file.Files.readAllBytes(file.toPath());
+ return new String(bytes, "UTF-8");
+ }
+
+ private static DocumentBuilderFactory secureDocumentBuilderFactory() throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setXIncludeAware(false);
+ try {
+ // Enable secure processing
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ } catch (Throwable ignore) {
+ // keep going for max compatibility with various parsers
+ }
+ try {
+ // Disallow loading external DTDs and external entities to avoid blocking/XXE
+ factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ } catch (Throwable ignore) {
+ }
+ try {
+ factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ } catch (Throwable ignore) {
+ }
+ try {
+ factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ } catch (Throwable ignore) {
+ }
+ // Do not expand entity references automatically
+ factory.setExpandEntityReferences(false);
+ return factory;
+ }
+}
diff --git a/src/main/resources/db_init.sql b/src/main/resources/db_init.sql
index 00faacf1..ddead61e 100644
--- a/src/main/resources/db_init.sql
+++ b/src/main/resources/db_init.sql
@@ -111,4 +111,30 @@ create table if not exists t_func_content
remark text,
create_time datetime,
modified_time datetime
+);
+
+-- text diff
+-- 文档主表
+CREATE TABLE IF NOT EXISTS doc (
+ doc_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE
+);
+
+-- 版本表
+CREATE TABLE IF NOT EXISTS doc_revision (
+ rev_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ doc_id INTEGER NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT (datetime('now')),
+ content TEXT NOT NULL,
+ FOREIGN KEY (doc_id) REFERENCES doc(doc_id)
+);
+
+-- 缓存行级 unified diff(从前一版本到本版本)
+CREATE TABLE IF NOT EXISTS doc_diff_cache (
+ from_rev_id INTEGER NOT NULL,
+ to_rev_id INTEGER NOT NULL,
+ unified TEXT NOT NULL,
+ PRIMARY KEY (from_rev_id, to_rev_id),
+ FOREIGN KEY (from_rev_id) REFERENCES doc_revision(rev_id),
+ FOREIGN KEY (to_rev_id) REFERENCES doc_revision(rev_id)
);
\ No newline at end of file