OSDN Git Service

Remove unused resource IDs
[winmerge-jp/winmerge-jp.git] / Src / MergeDoc.cpp
index 14a9386..3bc6318 100644 (file)
@@ -26,6 +26,7 @@
 #include "DirDoc.h"
 #include "files.h"
 #include "FileTransform.h"
+#include "Plugins.h"
 #include "unicoder.h"
 #include "UniFile.h"
 #include "OptionsDef.h"
 #include "MergeLineFlags.h"
 #include "FileOrFolderSelect.h"
 #include "LineFiltersList.h"
+#include "SubstitutionFiltersList.h"
 #include "TempFile.h"
 #include "codepage_detect.h"
-#include "SelectUnpackerDlg.h"
+#include "SelectPluginDlg.h"
 #include "EncodingErrorBar.h"
 #include "MergeCmdLineInfo.h"
 #include "TFile.h"
@@ -70,35 +72,61 @@ IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
 
 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
        //{{AFX_MSG_MAP(CMergeDoc)
+       // [File] menu
        ON_COMMAND(ID_FILE_SAVE, OnFileSave)
+       ON_UPDATE_COMMAND_UI(ID_FILE_SAVE, OnUpdateFileSave)
        ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
+       ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_LEFT, OnUpdateFileSaveLeft)
        ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
+       ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_MIDDLE, OnUpdateFileSaveMiddle)
        ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
+       ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_RIGHT, OnUpdateFileSaveRight)
        ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
-       ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
        ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
+       ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
        ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
-       ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
-       ON_UPDATE_COMMAND_UI(ID_STATUS_PLUGIN, OnUpdatePluginName)
-       ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
-       ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
+       ON_COMMAND(ID_FILE_LEFT_READONLY, OnFileReadOnlyLeft)
+       ON_UPDATE_COMMAND_UI(ID_FILE_LEFT_READONLY, OnUpdateFileReadOnlyLeft)
+       ON_COMMAND(ID_FILE_MIDDLE_READONLY, OnFileReadOnlyMiddle)
+       ON_UPDATE_COMMAND_UI(ID_FILE_MIDDLE_READONLY, OnUpdateFileReadOnlyMiddle)
+       ON_COMMAND(ID_FILE_RIGHT_READONLY, OnFileReadOnlyRight)
+       ON_UPDATE_COMMAND_UI(ID_FILE_RIGHT_READONLY, OnUpdateFileReadOnlyRight)
        ON_COMMAND(ID_RESCAN, OnFileReload)
        ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
-       ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
-       ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
-       ON_COMMAND(ID_POPUP_OPEN_WITH_UNPACKER, OnCtxtOpenWithUnpacker)
-       ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
-       ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
-       ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
-       ON_COMMAND(IDOK, OnOK)
        ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
        ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
        ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
        ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
-       ON_COMMAND(ID_MERGE_COMPARE_XML, OnFileRecompareAsXML)
-       ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_XML, OnUpdateFileRecompareAsXML)
-       ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
+       ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_WEBPAGE, OnFileRecompareAs)
+       ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
+       // [View] menu
+       ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
+       ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
+       ON_COMMAND(ID_SWAPPANES_SWAP12, (OnViewSwapPanes<0, 1>))
+       ON_COMMAND(ID_SWAPPANES_SWAP23, (OnViewSwapPanes<1, 2>))
+       ON_COMMAND(ID_SWAPPANES_SWAP13, (OnViewSwapPanes<0, 2>))
        ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
+       ON_COMMAND(ID_REFRESH, OnRefresh)
+       // [Tools] menu
+       ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
+       ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
+       // [Plugins] menu
+       ON_COMMAND(ID_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
+       ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
+       ON_COMMAND_RANGE(ID_NO_PREDIFFER, ID_NO_PREDIFFER, OnPrediffer)
+       ON_COMMAND_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnPrediffer)
+       ON_UPDATE_COMMAND_UI(ID_NO_PREDIFFER, OnUpdatePrediffer)
+       ON_UPDATE_COMMAND_UI_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnUpdatePrediffer)
+       // Encoding Error dialog
+       ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
+       ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
+       ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
+       ON_COMMAND(IDOK, OnOK)  
+       // Status bar
+       ON_UPDATE_COMMAND_UI(ID_STATUS_PANE0FILE_RO, OnUpdateStatusRO)
+       ON_UPDATE_COMMAND_UI(ID_STATUS_PANE1FILE_RO, OnUpdateStatusRO)
+       ON_UPDATE_COMMAND_UI(ID_STATUS_PANE2FILE_RO, OnUpdateStatusRO)
+       ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
        //}}AFX_MSG_MAP
 END_MESSAGE_MAP()
 
@@ -114,12 +142,14 @@ CMergeDoc::CMergeDoc()
 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
 , m_pDirDoc(nullptr)
 , m_bMixedEol(false)
-, m_pInfoUnpacker(new PackingInfo)
 , m_pEncodingErrorBar(nullptr)
 , m_bHasSyncPoints(false)
 , m_bAutoMerged(false)
 , m_nGroups(0)
 , m_pView{nullptr}
+, m_bAutomaticRescan(false)
+, m_CurrentPredifferID(0)
+, m_bChangedSchemeManually(false)
 {
        DIFFOPTIONS options = {0};
 
@@ -136,6 +166,8 @@ CMergeDoc::CMergeDoc()
        }
 
        m_bEnableRescan = true;
+       m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
+
        // COleDateTime m_LastRescan
        curUndo = undoTgt.begin();
        m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
@@ -155,6 +187,8 @@ CMergeDoc::CMergeDoc()
  */
 CMergeDoc::~CMergeDoc()
 {      
+       GetMainFrame()->UnwatchDocuments(this);
+
        if (m_pDirDoc != nullptr)
        {
                m_pDirDoc->MergeDocClosing(this);
@@ -213,7 +247,7 @@ void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
 {
        if (infoNewHandler != nullptr)
        {
-               *m_pInfoUnpacker = *infoNewHandler;
+               m_infoUnpacker = *infoNewHandler;
        }
 }
 
@@ -221,11 +255,19 @@ void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
 {
        m_diffWrapper.SetPrediffer(infoPrediffer);
 }
-void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
+
+void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer) const
 {
        m_diffWrapper.GetPrediffer(infoPrediffer);
 }
 
+const PrediffingInfo* CMergeDoc::GetPrediffer() const
+{
+       static PrediffingInfo infoPrediffer;
+       m_diffWrapper.GetPrediffer(&infoPrediffer);
+       return &infoPrediffer;
+}
+
 /////////////////////////////////////////////////////////////////////////////
 // CMergeDoc serialization
 
@@ -251,7 +293,7 @@ void CMergeDoc::Serialize(CArchive& ar)
 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
 {
        // and we don't repack the file
-       PackingInfo * tempPacker = nullptr;
+       PackingInfo tempPacker(false);
 
        // write buffer out to temporary file
        String sError;
@@ -301,16 +343,26 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
        {
                m_diffWrapper.SetFilterList(_T(""));
        }
+
+       if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
+       {
+               m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
+       }
+       else
+       {
+               m_diffWrapper.SetSubstitutionList(nullptr);
+       }
+
        if (GetView(0, 0)->m_CurSourceDef->type != 0)
                m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
        else
-               m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
+               m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_ptBuf[0]->m_strTempFileName.c_str(), m_strDesc[0].c_str()));
 
        for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
        {
                // Check if files have been modified since last rescan
                // Ignore checking in case of scratchpads (empty filenames)
-               if (!m_filePaths[nBuffer].empty())
+               if (!m_filePaths[nBuffer].empty() && !paths::IsURL(m_filePaths[nBuffer]))
                {
                        Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
                                        fileInfo, false, nBuffer);
@@ -388,8 +440,8 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
        else
        {
                const std::vector<std::vector<int> > syncpoints = GetSyncPointList();   
-               int nStartLine[3] = {0};
-               int nLines[3], nRealLine[3];
+               int nStartLine[3]{};
+               int nLines[3]{}, nRealLine[3]{};
                for (size_t i = 0; i <= syncpoints.size(); ++i)
                {
                        // Save text buffer to file
@@ -409,7 +461,9 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
 
                        // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
                        if (i == 0 && templist.GetSize() > 0)
+                       {
                                for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
+                               {
                                        if (nStartLine[nBuffer] == 0)
                                        {
                                                bool isEmptyFile = true;
@@ -432,6 +486,8 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
                                                        }
                                                }
                                        }
+                               }
+                       }
 
                        m_diffList.AppendDiffList(templist, nRealLine);
                        for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
@@ -484,8 +540,13 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
                        m_ptBuf[nBuffer]->prepareForRescan();
 
                // Divide diff blocks to match lines.
-               if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
-                       AdjustDiffBlocks();
+               if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES))
+               {
+                       if (m_nBuffers < 3)
+                               AdjustDiffBlocks();
+                       else
+                               AdjustDiffBlocks3way();
+               }
 
                // Analyse diff-list (updating real line-numbers)
                // this operation does not change the modified flag
@@ -497,7 +558,7 @@ int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
                // Apply flags to lines that are trivial
                PrediffingInfo infoPrediffer;
                GetPrediffer(&infoPrediffer);
-               if (!infoPrediffer.m_PluginName.empty())
+               if (!infoPrediffer.GetPluginPipeline().empty())
                        FlagTrivialLines();
                
                // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
@@ -538,7 +599,7 @@ void CMergeDoc::CheckFileChanged(void)
 {
        int nBuffer;
        DiffFileInfo fileInfo;
-       FileChange FileChange[3];
+       FileChange FileChange[3]{};
 
        for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
        {
@@ -583,7 +644,7 @@ void CMergeDoc::FlagTrivialLines(void)
                        for (int file = 0; file < m_nBuffers; ++file)
                        {
                                const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
-                               str[file] = p ? p : _T("");
+                               str[file] = p ? String(p, m_ptBuf[file]->GetFullLineLength(i)) : _T("");
                        }
 
                        if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
@@ -592,16 +653,24 @@ void CMergeDoc::FlagTrivialLines(void)
                                m_diffWrapper.GetOptions(&diffOptions);
 
                                // Make the call to stringdiffs, which does all the hard & tedious computations
-                               std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
+                               int result = strdiff::Compare(str[0], str[1],
                                        !diffOptions.bIgnoreCase,
                                        !diffOptions.bIgnoreEol,
                                        diffOptions.nIgnoreWhitespace,
-                                       GetBreakType(), // whitespace only or include punctuation
-                                       GetByteColoringOption());
-                               if (!worddiffs.empty())
+                                       diffOptions.bIgnoreNumbers);
+                               if (m_nBuffers >= 2 && result == 0)
+                               {
+                                       result = strdiff::Compare(str[1], str[2],
+                                               !diffOptions.bIgnoreCase,
+                                               !diffOptions.bIgnoreEol,
+                                               diffOptions.nIgnoreWhitespace,
+                                               diffOptions.bIgnoreNumbers);
+                               }
+                               if (result != 0)
                                {
                                        for (int file = 0; file < m_nBuffers; ++file)
                                                m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
+                                       ++m_nTrivialDiffs;
                                }
                        }
                }
@@ -756,7 +825,7 @@ void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
        // Files are not binaries, but they are identical
        if (identical != IDENTLEVEL::NONE)
        {
-               static_cast<CMergeFrameCommon*>(GetParentFrame())->ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
+               CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
                        [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
        }
 }
@@ -906,6 +975,84 @@ void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int la
        FlushAndRescan();
 }
 
+void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
+       int firstLineDiff, int lastLineDiff)
+{
+       lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
+       firstDiff = max(0, firstDiff);
+       if (firstDiff > lastDiff)
+               return;
+       
+       RescanSuppress suppressRescan(*this);
+
+       bool bGroupWithPrevious = false;
+       if (firstLineDiff <= 0 && lastLineDiff == -1)
+       {
+               if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
+                       return; // sync failure
+       }
+       else
+       {
+               if (!PartialListCopy(srcPane, dstPane, lastDiff, 
+                       (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
+                       return; // sync failure
+       }
+
+
+       SetEditedAfterRescan(dstPane);
+
+       int nGroup = GetActiveMergeView()->m_nThisGroup;
+       CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
+       CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
+       CPoint currentPosSrc = pViewSrc->GetCursorPos();
+       currentPosSrc.x = 0;
+       CPoint currentPosDst = pViewDst->GetCursorPos();
+       currentPosDst.x = 0;
+
+       CPoint pt(0, 0);
+       pViewDst->SetCursorPos(pt);
+       pViewDst->SetNewSelection(pt, pt, false);
+       pViewDst->SetNewAnchor(pt);
+
+       // copy from bottom up is more efficient
+       for (int i = lastDiff - 1; i >= firstDiff; --i)
+       {
+               if (m_diffList.IsDiffSignificant(i))
+               {
+                       SetCurrentDiff(i);
+                       const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
+                       if (currentPosDst.y > pdi->dend)
+                       {
+                               if (pdi->blank[dstPane] >= 0)
+                                       currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
+                               else if (pdi->blank[srcPane] >= 0)
+                                       currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
+                       }                       
+                       // Group merge with previous (merge undo data to one action)
+                       bGroupWithPrevious = true;
+                       if (i > firstDiff || firstLineDiff <= 0)
+                       {
+                               if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
+                                       break; // sync failure
+                       }
+                       else
+                       {
+                               if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
+                                       break; // sync failure
+                       }
+               }
+       }
+
+       ForEachView(dstPane, [currentPosDst](auto& pView) {
+               pView->SetCursorPos(currentPosDst);
+               pView->SetNewSelection(currentPosDst, currentPosDst, false);
+               pView->SetNewAnchor(currentPosDst);
+       });
+
+       suppressRescan.Clear(); // done suppress Rescan
+       FlushAndRescan();
+}
+
 enum MergeResult { NoMergeNeeded, Merged, Conflict };
 
 template<class Type>
@@ -943,6 +1090,8 @@ static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type ri
  */
 void CMergeDoc::DoAutoMerge(int dstPane)
 {
+       if (m_nBuffers < 3)
+               return;
        const int lastDiff = m_diffList.GetSize() - 1;
        const int firstDiff = 0;
        bool bGroupWithPrevious = false;
@@ -1034,7 +1183,7 @@ void CMergeDoc::DoAutoMerge(int dstPane)
 
        ShowMessageBox(
                strutils::format_string2(
-                       _T("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"), 
+                       _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"), 
                        strutils::format(_T("%d"), autoMergedCount),
                        strutils::format(_T("%d"), unresolvedConflictCount)),
                MB_ICONINFORMATION);
@@ -1050,7 +1199,7 @@ void CMergeDoc::DoAutoMerge(int dstPane)
  * @param [in] dr Difference to check.
  * @return true if difference lines match, false otherwise.
  */
-bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
+bool CMergeDoc::SanityCheckDiff(const DIFFRANGE& dr) const
 {
        const int cd_dbegin = dr.dbegin;
        const int cd_dend = dr.dend;
@@ -1201,11 +1350,102 @@ bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
        return true;
 }
 
+bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
+       bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
+{
+       int nGroup = GetActiveMergeView()->m_nThisGroup;
+       CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
+       CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
+
+       // suppress Rescan during this method
+       // (Not only do we not want to rescan a lot of times, but
+       // it will wreck the line status array to rescan as we merge)
+       RescanSuppress suppressRescan(*this);
+
+       DIFFRANGE cd;
+       VERIFY(m_diffList.GetDiff(nDiff, cd));
+       CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
+       CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
+       bool bSrcWasMod = sbuf.IsModified();
+       const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
+       const int cd_dend = cd.dend;
+       const int cd_blank = cd.blank[srcPane];
+       bool bInSync = SanityCheckDiff(cd);
+
+       if (!bInSync)
+       {
+               LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
+               return false; // abort copying
+       }
+
+       // If we remove whole diff from current view, we must fix cursor
+       // position first. Normally we would move to end of previous line,
+       // but we want to move to begin of that line for usability.
+       if (bUpdateView)
+       {
+               CPoint currentPos = pViewDst->GetCursorPos();
+               currentPos.x = 0;
+               if (currentPos.y > cd_dend)
+               {
+                       if (cd.blank[dstPane] >= 0)
+                               currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
+                       else if (cd.blank[srcPane] >= 0)
+                               currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
+               }
+               ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
+       }
+
+       // if the current diff contains missing lines, remove them from both sides
+       int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
+
+       // curView is the view which is changed, so the opposite of the source view
+       dbuf.BeginUndoGroup(bGroupWithPrevious);
+       if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
+       {
+               // text was missing, so delete rest of lines on both sides
+               // delete only on destination side since rescan will clear the other side
+               if (limit+1 < dbuf.GetLineCount())
+               {
+                       dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
+               }
+               else
+               {
+                       // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
+                       ASSERT(cd_dbegin > 0);
+                       dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
+               }
+
+               limit = cd_dbegin-1;
+               dbuf.FlushUndoGroup(pSource);
+               dbuf.BeginUndoGroup(true);
+       }
+
+       // copy the selected text over
+       if (cd_dbegin <= limit)
+       {
+               // text exists on left side, so just replace
+               dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
+               dbuf.FlushUndoGroup(pSource);
+               dbuf.BeginUndoGroup(true);
+       }
+       dbuf.FlushUndoGroup(pSource);
+
+       // remove the diff
+       SetCurrentDiff(-1);
+
+       // reset the mod status of the source view because we do make some
+       // changes, but none that concern the source text
+       sbuf.SetModified(bSrcWasMod);
+
+       suppressRescan.Clear(); // done suppress Rescan
+       FlushAndRescan();
+       return true;
+}
+
 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
                const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
 {
        int nGroup = GetActiveMergeView()->m_nThisGroup;
-       CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
        CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
        CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
 
@@ -1260,9 +1500,6 @@ bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordD
                ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
        }
 
-       // if the current diff contains missing lines, remove them from both sides
-       int limit = cd_dend;
-
        // curView is the view which is changed, so the opposite of the source view
        dbuf.BeginUndoGroup(bGroupWithPrevious);
 
@@ -1346,7 +1583,7 @@ bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordD
  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
  */
 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
-       int nBuffer, PackingInfo * pInfoTempUnpacker)
+       int nBuffer, PackingInfo& infoTempUnpacker)
 {
        String s;
        String str;
@@ -1361,24 +1598,9 @@ bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
        // Select message based on reason function called
        if (nSaveResult == SAVE_PACK_FAILED)
        {
-               if (m_nBuffers == 3)
-               {
-                       str = strutils::format_string2(
-                               nBuffer == 0 ? 
-                                       _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")
-                                       : (nBuffer == 1 ? 
-                                       _("Plugin '%2' cannot pack your changes to the middle file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"): 
-                                       _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")),
-                               strPath, pInfoTempUnpacker->m_PluginName);
-               }
-               else
-               {
-                       str = strutils::format_string2(nBuffer == 0 ? _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?") : 
-                               _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"),
-                               strPath, pInfoTempUnpacker->m_PluginName);
-               }
+               str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
                // replace the unpacker with a "do nothing" unpacker
-               pInfoTempUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
+               infoTempUnpacker.Initialize(false);
        }
        else
        {
@@ -1403,9 +1625,9 @@ bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
                if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
                {
                        CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
-                       strSavePath = s;
+                       strSavePath = std::move(s);
                        nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
-                               pInfoTempUnpacker);
+                               infoTempUnpacker);
 
                        if (nSaveResult == SAVE_DONE)
                        {
@@ -1474,9 +1696,9 @@ bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
        }
 
        // use a temp packer
-       // first copy the m_pInfoUnpacker
+       // first copy the m_infoUnpacker
        // if an error arises during packing, change and take a "do nothing" packer
-       PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
+       PackingInfo infoTempUnpacker = m_infoUnpacker;
 
        bSaveSuccess = false;
        
@@ -1495,11 +1717,11 @@ bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
                        strSavePath = theApp.m_strSaveAsPath;   
        }
 
-       nRetVal = theApp.HandleReadonlySave(strSavePath, false, bApplyToAll);
+       nRetVal = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
        if (nRetVal == IDCANCEL)
                return false;
 
-       if (!theApp.CreateBackup(false, strSavePath))
+       if (!CMergeApp::CreateBackup(false, strSavePath))
                return false;
 
        // false as long as the user is not satisfied
@@ -1523,13 +1745,13 @@ bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
        String sError;
        if (nSaveErrorCode == SAVE_DONE)
                // We have a filename, just try to save
-               nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, &infoTempUnpacker);
+               nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, infoTempUnpacker);
 
        if (nSaveErrorCode != SAVE_DONE)
        {
                // Saving failed, user may save to another location if wants to
                do
-                       result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
+                       result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
                while (!result);
        }
 
@@ -1551,9 +1773,9 @@ bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
                }
 
                m_ptBuf[nBuffer]->SetModified(false);
-               m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
-               m_filePaths[nBuffer] = strSavePath;
-               m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
+               m_pSaveFileInfo[nBuffer]->Update(strSavePath);
+               m_filePaths[nBuffer] = std::move(strSavePath);
+               m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
                UpdateHeaderPath(nBuffer);
                bSaveSuccess = true;
                result = true;
@@ -1588,9 +1810,9 @@ bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
        String strSavePath(szPath);
 
        // use a temp packer
-       // first copy the m_pInfoUnpacker
+       // first copy the m_infoUnpacker
        // if an error arises during packing, change and take a "do nothing" packer
-       PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
+       PackingInfo infoTempUnpacker = m_infoUnpacker;
 
        bSaveSuccess = false;
        // false as long as the user is not satisfied
@@ -1607,14 +1829,14 @@ bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
        // Loop until user succeeds saving or cancels
        String sError;
        do
-               result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
+               result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
        while (!result);
 
        // Saving succeeded with given/selected filename
        if (nSaveErrorCode == SAVE_DONE)
        {
                m_pSaveFileInfo[nBuffer]->Update(strSavePath);
-               m_filePaths[nBuffer] = strSavePath;
+               m_filePaths[nBuffer] = std::move(strSavePath);
                m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
                UpdateHeaderPath(nBuffer);
                bSaveSuccess = true;
@@ -1752,6 +1974,7 @@ void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
        if (nRescanResult != RESCAN_SUPPRESSED)
                ShowRescanError(nRescanResult, identical);
        m_LastRescan = COleDateTime::GetCurrentTime();
+       SetTitle(nullptr);
 }
 
 /**
@@ -1859,6 +2082,44 @@ void CMergeDoc::OnFileSaveRight()
 }
 
 /**
+ * @brief Called when "Save" item is updated
+ */
+void CMergeDoc::OnUpdateFileSave(CCmdUI* pCmdUI)
+{
+       bool bModified = false;
+       for (int nPane = 0; nPane < m_nBuffers; nPane++)
+       {
+               if (m_ptBuf[nPane]->IsModified())
+                       bModified = true;
+       }
+       pCmdUI->Enable(bModified);
+}
+
+/**
+ * @brief Called when "Save left (as...)" item is updated
+ */
+void CMergeDoc::OnUpdateFileSaveLeft(CCmdUI* pCmdUI)
+{
+       pCmdUI->Enable(m_ptBuf[0]->IsModified());
+}
+
+/**
+ * @brief Called when "Save middle (as...)" item is updated
+ */
+void CMergeDoc::OnUpdateFileSaveMiddle(CCmdUI* pCmdUI)
+{
+       pCmdUI->Enable(m_nBuffers == 3 && m_ptBuf[1]->IsModified());
+}
+
+/**
+ * @brief Called when "Save right (as...)" item is updated
+ */
+void CMergeDoc::OnUpdateFileSaveRight(CCmdUI* pCmdUI)
+{
+       pCmdUI->Enable(m_ptBuf[m_nBuffers - 1]->IsModified());
+}
+
+/**
  * @brief Saves left-side file with name asked
  */
 void CMergeDoc::OnFileSaveAsLeft()
@@ -1894,6 +2155,82 @@ void CMergeDoc::OnFileSaveAsRight()
 }
 
 /**
+ * @brief Enable/disable left buffer read-only
+ */
+void CMergeDoc::OnFileReadOnlyLeft()
+{
+       bool bReadOnly = m_ptBuf[0]->GetReadOnly();
+       m_ptBuf[0]->SetReadOnly(!bReadOnly);
+}
+
+/**
+ * @brief Called when "Left read-only" item is updated
+ */
+void CMergeDoc::OnUpdateFileReadOnlyLeft(CCmdUI* pCmdUI)
+{
+       bool bReadOnly = m_ptBuf[0]->GetReadOnly();
+       pCmdUI->Enable(true);
+       pCmdUI->SetCheck(bReadOnly);
+}
+
+/**
+ * @brief Enable/disable middle buffer read-only
+ */
+void CMergeDoc::OnFileReadOnlyMiddle()
+{
+       if (m_nBuffers == 3)
+       {
+               bool bReadOnly = m_ptBuf[1]->GetReadOnly();
+               m_ptBuf[1]->SetReadOnly(!bReadOnly);
+       }
+}
+
+/**
+ * @brief Called when "Middle read-only" item is updated
+ */
+void CMergeDoc::OnUpdateFileReadOnlyMiddle(CCmdUI* pCmdUI)
+{
+       if (m_nBuffers < 3)
+       {
+               pCmdUI->Enable(false);
+       }
+       else
+       {
+               bool bReadOnly = m_ptBuf[1]->GetReadOnly();
+               pCmdUI->Enable(true);
+               pCmdUI->SetCheck(bReadOnly);
+       }
+}
+
+/**
+ * @brief Enable/disable right buffer read-only
+ */
+void CMergeDoc::OnFileReadOnlyRight()
+{
+       bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
+       m_ptBuf[m_nBuffers - 1]->SetReadOnly(!bReadOnly);
+}
+
+/**
+ * @brief Called when "Left read-only" item is updated
+ */
+void CMergeDoc::OnUpdateFileReadOnlyRight(CCmdUI* pCmdUI)
+{
+       bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
+       pCmdUI->Enable(true);
+       pCmdUI->SetCheck(bReadOnly);
+}
+
+/**
+ * @brief Update readonly statusbaritem
+ */
+void CMergeDoc::OnUpdateStatusRO(CCmdUI* pCmdUI)
+{
+       bool bRO = m_ptBuf[pCmdUI->m_nID - ID_STATUS_PANE0FILE_RO]->GetReadOnly();
+       pCmdUI->Enable(bRO);
+}
+
+/**
  * @brief Update diff-number pane text in file compare.
  * The diff number pane shows selected difference/amount of differences when
  * there is difference selected. If there is no difference selected, then
@@ -1903,7 +2240,6 @@ void CMergeDoc::OnFileSaveAsRight()
  */
 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI) 
 {
-       TCHAR sIdx[32] = { 0 };
        TCHAR sCnt[32] = { 0 };
        String s;
        const int nDiffs = m_diffList.GetSignificantDiffs();
@@ -1925,6 +2261,7 @@ void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
        // - show diff number and amount of diffs
        else
        {
+               TCHAR sIdx[32] = { 0 };
                s = _("Difference %1 of %2");
                const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
                _itot_s(signInd + 1, sIdx, 10);
@@ -1936,22 +2273,6 @@ void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
 }
 
 /**
- * @brief Update plugin name
- * @param [in] pCmdUI UI component to update.
- */
-void CMergeDoc::OnUpdatePluginName(CCmdUI* pCmdUI)
-{
-       String pluginNames;
-       if (m_pInfoUnpacker && !m_pInfoUnpacker->m_PluginName.empty())
-               pluginNames += m_pInfoUnpacker->m_PluginName + _T("&");
-       PrediffingInfo prediffer;
-       GetPrediffer(&prediffer);
-       if (!prediffer.m_PluginName.empty())
-               pluginNames += prediffer.m_PluginName + _T("&");
-       pCmdUI->SetText(pluginNames.substr(0, pluginNames.length() - 1).c_str());
-}
-
-/**
  * @brief Change number of diff context lines
  */
 void CMergeDoc::OnDiffContext(UINT nID)
@@ -1983,6 +2304,16 @@ void CMergeDoc::OnDiffContext(UINT nID)
        GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
        GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
        FlushAndRescan(true);
+       ForEachView([](auto& pView) { if (pView->m_bDetailView) pView->EnsureVisible(pView->GetCursorPos()); });
+}
+
+/**
+ * @brief Swap the positions of the two panes
+ */
+template<int srcPane, int dstPane>
+void CMergeDoc::OnViewSwapPanes()
+{
+       SwapFiles(srcPane, dstPane);
 }
 
 /**
@@ -1990,14 +2321,7 @@ void CMergeDoc::OnDiffContext(UINT nID)
  */
 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
 {
-       if (m_nBuffers > 2)
-       {
-               pCmdUI->Enable(true);
-       }
-       else
-       {
-               pCmdUI->Enable(false);
-       }
+       pCmdUI->Enable(m_nBuffers > 2);
 }
 
 /**
@@ -2032,6 +2356,15 @@ void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
 }
 
 /**
+ * @brief Refresh display using text-buffers
+ * @note This DOES NOT reload files!
+ */
+void CMergeDoc::OnRefresh()
+{
+       FlushAndRescan(true);
+}
+
+/**
  * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
  *
  * @note After PrimeTextBuffers(), all buffers should have the same length.
@@ -2286,6 +2619,12 @@ void CMergeDoc::HideLines()
        ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
 }
 
+void CMergeDoc::AddToLineFilters(const String& text)
+{
+       theApp.m_pLineFilters->AddFilter(strutils::to_regex(text), true);
+       theApp.m_pLineFilters->SaveFilters();
+}
+
 /**
  * @brief Asks and then saves modified files.
  *
@@ -2418,17 +2757,24 @@ void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
  * @brief We have two child views (left & right), so we keep pointers directly
  * at them (the MFC view list doesn't have them both)
  */
-void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
+void CMergeDoc::AddMergeViews(CMergeEditSplitterView* pMergeEditSplitterView, CMergeEditView *pView[3])
 {
-
+       m_pMergeEditSplitterView[m_nGroups] = pMergeEditSplitterView;
        for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
                m_pView[m_nGroups][nBuffer] = pView[nBuffer];
        ++m_nGroups;
 }
 
-void CMergeDoc::RemoveMergeViews(int nGroup)
+void CMergeDoc::RemoveMergeViews(CMergeEditSplitterView* pMergeEditSplitterView)
 {
-
+       ASSERT(m_nGroups > 0);
+       int nGroup;
+       for (nGroup = 0; nGroup < m_nGroups; ++nGroup)
+       {
+               if (m_pMergeEditSplitterView[nGroup] == pMergeEditSplitterView)
+                       break;
+       }
+       ASSERT(nGroup < m_nGroups);
        for (; nGroup < m_nGroups - 1; nGroup++)
        {
                for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
@@ -2436,7 +2782,11 @@ void CMergeDoc::RemoveMergeViews(int nGroup)
                        m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
                        m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
                }
+               m_pMergeEditSplitterView[nGroup] = m_pMergeEditSplitterView[nGroup + 1];
        }
+       for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
+               m_pView[m_nGroups - 1][nBuffer] = nullptr;
+       m_pMergeEditSplitterView[m_nGroups - 1] = nullptr;
        --m_nGroups;
 }
 
@@ -2476,11 +2826,21 @@ bool CMergeDoc::CloseNow()
        if (!PromptAndSaveIfNeeded(true))
                return false;
 
-       GetParentFrame()->CloseNow();
+       GetParentFrame()->DestroyWindow();
        return true;
 }
 
 /**
+ * @brief A string to display as a tooltip for MDITabbar
+ */
+CString CMergeDoc::GetTooltipString() const
+{
+       PrediffingInfo infoPrediffer;
+       GetPrediffer(&infoPrediffer);
+       return CMergeFrameCommon::GetTooltipString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer, m_nTrivialDiffs > 0).c_str();
+}
+
+/**
  * @brief Loads file to buffer and shows load-errors
  * @param [in] sFileName File to open
  * @param [in] nBuffer Index (0-based) of buffer to load
@@ -2499,7 +2859,7 @@ int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const F
 
        CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
        CString sOpenError;
-       retVal = pBuf->LoadFromFile(sFileName, m_pInfoUnpacker.get(),
+       retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
                m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
 
        // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
@@ -2572,7 +2932,7 @@ void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
  * @param [in] encoding File's encoding.
  * @return One of FileLoadResult values.
  */
-DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc, 
+DWORD CMergeDoc::LoadOneFile(int index, const String& filename, bool readOnly, const String& strDesc, 
                const FileTextEncoding & encoding)
 {
        DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
@@ -2590,9 +2950,13 @@ DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const St
                loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
                if (FileLoadResult::IsLossy(loadSuccess))
                {
-                       m_ptBuf[index]->FreeAll();
-                       loadSuccess = LoadFile(filename.c_str(), index, readOnly,
-                               GuessCodepageEncoding(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1));
+                       // Determine the file encoding by looking at all the contents of the file, not just part of it
+                       FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
+                       if (encoding != encodingNew)
+                       {
+                               m_ptBuf[index]->FreeAll();
+                               loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
+                       }
                }
        }
        else
@@ -2606,58 +2970,72 @@ DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const St
        return loadSuccess;
 }
 
-void CMergeDoc::SetTableProperties()
+CMergeDoc::TableProps CMergeDoc::MakeTablePropertiesByFileName(const String& path, const std::optional<bool>& enableTableEditing, bool showDialog)
 {
-       struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
-       auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
+       const TCHAR quote = strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR));
+       FileFilterHelper filterCSV, filterTSV, filterDSV;
+       bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
+       const String& csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
+       if (!csvFilePattern.empty())
        {
-               const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
-               FileFilterHelper filterCSV, filterTSV, filterDSV;
-               bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
-               const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
-               if (!csvFilePattern.empty())
-               {
-                       filterCSV.UseMask(true);
-                       filterCSV.SetMask(csvFilePattern);
-                       if (filterCSV.includeFile(path))
-                               return { true, ',', quote, allowNewlineIQuotes };
-               }
-               const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
-               if (!tsvFilePattern.empty())
-               {
-                       filterTSV.UseMask(true);
-                       filterTSV.SetMask(tsvFilePattern);
-                       if (filterTSV.includeFile(path))
-                               return { true, '\t', quote, allowNewlineIQuotes };
-               }
-               const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
-               if (!dsvFilePattern.empty())
-               {
-                       filterDSV.UseMask(true);
-                       filterDSV.SetMask(dsvFilePattern);
-                       if (filterDSV.includeFile(path))
-                               return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
-               }
-               if (enableTableEditing.value_or(false))
+               filterCSV.UseMask(true);
+               filterCSV.SetMask(csvFilePattern);
+               if (filterCSV.includeFile(path))
+                       return { true, ',', quote, allowNewlineIQuotes };
+       }
+       const String& tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
+       if (!tsvFilePattern.empty())
+       {
+               filterTSV.UseMask(true);
+               filterTSV.SetMask(tsvFilePattern);
+               if (filterTSV.includeFile(path))
+                       return { true, '\t', quote, allowNewlineIQuotes };
+       }
+       const String& dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
+       if (!dsvFilePattern.empty())
+       {
+               filterDSV.UseMask(true);
+               filterDSV.SetMask(dsvFilePattern);
+               if (filterDSV.includeFile(path))
+                       return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
+       }
+       if (enableTableEditing.value_or(false))
+       {
+               if (showDialog)
                {
                        COpenTableDlg dlg;
                        if (dlg.DoModal() == IDOK)
-                               return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
+                               return { true, strutils::from_charstr(dlg.m_sDelimiterChar), strutils::from_charstr(dlg.m_sQuoteChar), dlg.m_bAllowNewlinesInQuotes };
+               }
+               else
+               {
+                       return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
                }
-               return { false, 0, 0, false };
-       };
+       }
+       return { false, 0, 0, false };
+};
 
-       TableProps tableProps[3] = {};
+void CMergeDoc::SetTableProperties()
+{
+       TableProps tableProps[3] = { };
        int nTableFileIndex = -1;
-       for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
+       if (m_pTablePropsPrepared)
        {
-               if (nBuffer == 0 ||
-                       paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
-                       tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
-               else
-                       tableProps[nBuffer] = tableProps[nBuffer - 1];
-               if (tableProps[nBuffer].istable)
-                       nTableFileIndex = nBuffer;
+               nTableFileIndex = 0;
+               tableProps[0] = *m_pTablePropsPrepared;
+       }
+       else
+       {
+               for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
+               {
+                       if (nBuffer == 0 ||
+                               paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
+                               tableProps[nBuffer] = MakeTablePropertiesByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
+                       else
+                               tableProps[nBuffer] = tableProps[nBuffer - 1];
+                       if (tableProps[nBuffer].istable)
+                               nTableFileIndex = nBuffer;
+               }
        }
        for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
        {
@@ -2678,17 +3056,38 @@ void CMergeDoc::SetTableProperties()
        }
 }
 
+void CMergeDoc::SetTextType(int textType)
+{
+       ForEachView([textType, this](auto& pView) {
+               pView->SetTextType(CrystalLineParser::TextType(textType));
+               pView->SetDisableBSAtSOL(false);
+               m_bChangedSchemeManually = true;
+       });
+}
+
+void CMergeDoc::SetTextType(const String& ext)
+{
+       String ext2 = ext;
+       strutils::replace(ext2, _T("."), _T(""));
+       ForEachView([&ext2, this](auto& pView) {
+               pView->SetTextType(ext2.c_str());
+               pView->SetDisableBSAtSOL(false);
+               m_bChangedSchemeManually = true;
+       });
+}
+
 /**
  * @brief Loads files and does initial rescan.
  * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
  * @param bRO [in] Is left/middle/right file read-only
  * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
  * @todo Options are still read from CMainFrame, this will change
- * @sa CMainFrame::ShowMergeDoc()
+ * @sa CMainFrame::ShowTextMergeDoc()
  */
 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
                const bool bRO[], const String strDesc[])
 {
+       CWaitCursor waitstatus;
        IDENTLEVEL identical = IDENTLEVEL::NONE;
        int nRescanResult = RESCAN_OK;
        int nBuffer;
@@ -2724,11 +3123,21 @@ bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
        m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
 
        // Load files
-       DWORD nSuccess[3];
+       DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR };
        for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
        {
                nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
                        fileloc[nBuffer].encoding);
+               if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
+               {
+                       CMergeEditFrame* pFrame = GetParentFrame();
+                       if (pFrame != nullptr)
+                       {
+                               // Use verify macro to trap possible error in debug.
+                               VERIFY(pFrame->DestroyWindow());
+                       }
+                       return false;
+               }
        }
 
        SetTableProperties();
@@ -2741,20 +3150,8 @@ bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
        { 
                if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
                {
-                       m_pInfoUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
-               }
-       }
-
-       // Bail out if either side failed
-       if (std::find_if(nSuccess, nSuccess + m_nBuffers, [](DWORD d){return !FileLoadResult::IsOk(d);} ) != nSuccess + m_nBuffers)
-       {
-               CMergeEditFrame *pFrame = GetParentFrame();
-               if (pFrame != nullptr)
-               {
-                       // Use verify macro to trap possible error in debug.
-                       VERIFY(pFrame->DestroyWindow());
+                       m_infoUnpacker.Initialize(false);
                }
-               return false;
        }
 
        // Warn user if file load was lossy (bad encoding)
@@ -2853,15 +3250,12 @@ bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
                // Note: If option enabled, and another side type is not recognized,
                // we use recognized type for unrecognized side too.
                String sext[3];
-               bool bTyped[3];
+               bool bTyped[3]{};
                int paneTyped = 0;
 
                for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
                {
-                       if (bFiltersEnabled && m_pInfoUnpacker->m_textType.length())
-                               sext[nBuffer] = m_pInfoUnpacker->m_textType;
-                       else
-                               sext[nBuffer] = GetFileExt(fileloc[nBuffer].filepath.c_str(), m_strDesc[nBuffer].c_str());
+                       sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
                        ForEachView(nBuffer, [&](auto& pView) {
                                bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
                                if (bTyped[nBuffer])
@@ -2941,14 +3335,16 @@ bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
        if (m_pView[0][0] != nullptr)
                m_pView[0][0]->RepaintLocationPane();
 
+       GetMainFrame()->WatchDocuments(this);
+
        return true;
 }
 
-void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
+void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex, bool bRealLine, int nCharIndex)
 {
        if (nPane < 0)
        {
-               nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
+               nPane = (m_nBufferType[0] != BUFFERTYPE::UNNAMED) ? GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE) : 0;
                if (nPane < 0 || nPane >= m_nBuffers)
                        nPane = 0;
        }
@@ -2965,7 +3361,7 @@ void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
                        return;
                }
        }
-       m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, false, nPane);
+       m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, bRealLine, nPane, true, nCharIndex);
 }
 
 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
@@ -2975,7 +3371,7 @@ void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
 
        FileLocation fileloc[3];
        String strDesc[3];
-       bool bRO[3];
+       bool bRO[3]{};
        for (int pane = 0; pane < m_nBuffers; pane++)
        {
                bRO[pane] = m_ptBuf[pane]->GetReadOnly();
@@ -2985,12 +3381,23 @@ void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
        }
        std::copy_n(m_strDesc, m_nBuffers, strDesc);
 
-       strDesc[nBuffer] = _T("");
+       strDesc[nBuffer].clear();
        fileloc[nBuffer].setPath(path);
-       fileloc[nBuffer].encoding = GuessCodepageEncoding(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
+       fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
+
+       bool filenameChanged = path != m_filePaths[nBuffer];
+       auto columnWidths = m_ptBuf[nBuffer]->GetColumnWidths();
+       int nActivePane = GetActiveMergeView()->m_nThisPane;
        
        if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
-               MoveOnLoad(nBuffer, nLineIndex);
+       {
+               // Restore column widths and active pane changed by OpenDocs to their previous state
+               if (!filenameChanged)
+               {
+                       m_ptBuf[nBuffer]->SetColumnWidths(columnWidths);
+               }
+               MoveOnLoad(nActivePane, nLineIndex);
+       }
 }
 
 /**
@@ -3004,6 +3411,8 @@ void CMergeDoc::RefreshOptions()
 {
        DIFFOPTIONS options = {0};
        
+       m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
+
        m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
        Options::DiffOptions::Load(GetOptionsMgr(), options);
 
@@ -3042,6 +3451,11 @@ void CMergeDoc::UpdateHeaderPath(int pane)
        if (bChanges)
                sText.insert(0, _T("* "));
 
+       if (m_sCurrentHeaderTitle[pane] == sText)
+               return;
+
+       m_sCurrentHeaderTitle[pane] = sText;
+
        pf->GetHeaderInterface()->SetText(pane, sText);
 
        SetTitle(nullptr);
@@ -3105,20 +3519,9 @@ bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
  */
 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
 {
-       String sTitle;
-       String sFileName[3];
-
-       if (lpszTitle != nullptr)
-               sTitle = lpszTitle;
-       else
-       {
-               for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
-                       sFileName[nBuffer] = !m_strDesc[nBuffer].empty() ? m_strDesc[nBuffer] : paths::FindFileName(m_filePaths[nBuffer]);
-               if (std::count(&sFileName[0], &sFileName[0] + m_nBuffers, sFileName[0]) == m_nBuffers)
-                       sTitle = sFileName[0] + strutils::format(_T(" x %d"), m_nBuffers);
-               else
-                       sTitle = strutils::join(&sFileName[0], &sFileName[0] + m_nBuffers, _T(" - "));
-       }
+       PrediffingInfo infoPrediffer;
+       GetPrediffer(&infoPrediffer);
+       String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer, m_nTrivialDiffs > 0);
        CDocument::SetTitle(sTitle.c_str());
 }
 
@@ -3204,41 +3607,6 @@ void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
 }
 
 /**
- * @brief Display unpacker dialog to user & handle user's choices
- */
-bool CMergeDoc::OpenWithUnpackerDialog()
-{
-       // let the user choose a handler
-       CSelectUnpackerDlg dlg(m_filePaths[0], nullptr);
-       // create now a new infoUnpacker to initialize the manual/automatic flag
-       PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_AUTO);
-       dlg.SetInitialInfoHandler(&infoUnpacker);
-
-       if (dlg.DoModal() == IDOK)
-       {
-               infoUnpacker = dlg.GetInfoHandler();
-               Merge7zFormatMergePluginScope scope(&infoUnpacker);
-               if (HasZipSupport() && std::count_if(m_filePaths.begin(), m_filePaths.end(), ArchiveGuessFormat) == m_nBuffers)
-               {
-                       DWORD dwFlags[3] = {FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU};
-                       GetMainFrame()->DoFileOpen(&m_filePaths, dwFlags, m_strDesc, _T(""), 
-                               GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, _T(""), &infoUnpacker);
-                       CloseNow();
-               }
-               else
-               {
-                       SetUnpacker(&infoUnpacker);
-                       OnFileReload();
-               }
-               return true;
-       }
-       else
-       {
-               return false;
-       }
-}
-
-/**
  * @brief Reloads the opened files
  */
 void CMergeDoc::OnFileReload()
@@ -3247,7 +3615,7 @@ void CMergeDoc::OnFileReload()
                return;
        
        FileLocation fileloc[3];
-       bool bRO[3];
+       bool bRO[3]{};
        for (int pane = 0; pane < m_nBuffers; pane++)
        {
                bRO[pane] = m_ptBuf[pane]->GetReadOnly();
@@ -3255,9 +3623,15 @@ void CMergeDoc::OnFileReload()
                fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
                fileloc[pane].setPath(m_filePaths[pane]);
        }
+       int nActivePane = GetActiveMergeView()->m_nThisPane;
        CPoint pt = GetActiveMergeView()->GetCursorPos();
+       auto columnWidths = m_ptBuf[0]->GetColumnWidths();
        if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
-               MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
+       {
+               // Restore column widths and active pane changed by OpenDocs to their previous state
+               m_ptBuf[0]->SetColumnWidths(columnWidths);
+               MoveOnLoad(nActivePane, pt.y);
+       }
 }
 
 /**
@@ -3268,9 +3642,175 @@ void CMergeDoc::OnFileEncoding()
        DoFileEncodingDialog();
 }
 
-void CMergeDoc::OnCtxtOpenWithUnpacker() 
+void CMergeDoc::OnOpenWithUnpacker()
+{
+       CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
+               strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
+               CSelectPluginDlg::PluginType::Unpacker, false);
+       if (dlg.DoModal() != IDOK)
+               return;
+
+       if (!PromptAndSaveIfNeeded(true))
+               return;
+
+       PackingInfo infoUnpacker(dlg.GetPluginPipeline());
+       PathContext paths = m_filePaths;
+       DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
+       String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
+       int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
+       nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -nID;
+
+       if (GetMainFrame()->DoFileOrFolderOpen(&paths, dwFlags, strDesc, _T(""),
+               GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, &infoUnpacker, nullptr, nID))
+               GetParentFrame()->DestroyWindow();
+}
+
+void CMergeDoc::OnApplyPrediffer() 
+{
+       PrediffingInfo prediffer;
+       GetPrediffer(&prediffer);
+       // let the user choose a handler
+       CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
+               strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
+               CSelectPluginDlg::PluginType::Prediffer, false);
+       if (dlg.DoModal() != IDOK)
+               return;
+       prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
+       SetPrediffer(&prediffer);
+       m_CurrentPredifferID = -1;
+       FlushAndRescan(true);
+}
+
+/**
+ * @brief Create the dynamic submenu for prediffers
+ *
+ * @note The plugins are grouped in (suggested) and (not suggested)
+ *       The IDs follow the order of GetAvailableScripts
+ *       For example :
+ *                             suggested 0         ID_1ST + 0 
+ *                             suggested 1         ID_1ST + 2 
+ *                             suggested 2         ID_1ST + 5 
+ *                             not suggested 0     ID_1ST + 1 
+ *                             not suggested 1     ID_1ST + 3 
+ *                             not suggested 2     ID_1ST + 4 
+ */
+HMENU CMergeDoc::createPrediffersSubmenu(HMENU hMenu)
+{
+       // empty the menu
+       int j = GetMenuItemCount(hMenu);
+       while (j --)
+               DeleteMenu(hMenu, 0, MF_BYPOSITION);
+
+       // title
+       AppendMenu(hMenu, MF_STRING, ID_NO_PREDIFFER, _("No prediffer (normal)").c_str());
+       
+       if (!GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED))
+               return hMenu;
+
+       m_CurrentPredifferID = -1;
+
+       // compute the m_CurrentPredifferID (to set the radio button)
+       PrediffingInfo prediffer;
+       GetPrediffer(&prediffer);
+       if (prediffer.GetPluginPipeline().empty())
+               m_CurrentPredifferID = ID_NO_PREDIFFER;
+
+       // get the scriptlet files
+       const auto& [ suggestedPlugins, allPlugins ]= FileTransform::CreatePluginMenuInfos(
+               m_strBothFilenames, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
+
+       // build the menu : first part, suggested plugins
+       // title
+       AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
+       AppendMenu(hMenu, MF_STRING, ID_SUGGESTED_PLUGINS, _("Suggested plugins").c_str());
+
+       for (const auto& [caption, name, id, plugin ] : suggestedPlugins)
+               AppendMenu(hMenu, MF_STRING, id, caption.c_str());
+
+       // build the menu : second part, others plugins
+       // title
+       AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
+       AppendMenu(hMenu, MF_STRING, ID_NOT_SUGGESTED_PLUGINS, _("All plugins").c_str());
+
+       String lastPluginName;
+       String errorMessage;
+       auto result = prediffer.ParsePluginPipeline(errorMessage);
+       if (result.size() > 0)
+               lastPluginName = result.back().name;
+       
+       for (const auto& [processType, pluginAry] : allPlugins)
+       {
+               for (const auto& [caption, name, id, plugin] : pluginAry)
+               {
+                       if (!name.empty())
+                       {
+                               AppendMenu(hMenu, MF_STRING, id, caption.c_str());
+                               if (lastPluginName == plugin->m_name)
+                                       m_CurrentPredifferID = id;
+                       }
+               }
+       }
+
+       return hMenu;
+}
+
+/**
+ * @brief Called when an editor script item is updated
+ */
+void CMergeDoc::OnUpdatePrediffer(CCmdUI* pCmdUI)
+{
+       pCmdUI->Enable(true);
+
+       PrediffingInfo prediffer;
+       GetPrediffer(&prediffer);
+
+       if (prediffer.GetPluginPipeline().find(_T("<Automatic>")) != String::npos)
+       {
+               pCmdUI->SetRadio(false);
+               return;
+       }
+
+       // Detect when CDiffWrapper::RunFileDiff has canceled a buggy prediffer
+       if (prediffer.GetPluginPipeline().empty())
+               m_CurrentPredifferID = ID_NO_PREDIFFER;
+
+       pCmdUI->SetRadio(pCmdUI->m_nID == static_cast<UINT>(m_CurrentPredifferID));
+}
+
+/**
+ * @brief Handler for all prediffer choices, including ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO, ID_NO_PREDIFFER, & specific prediffers
+ */
+void CMergeDoc::OnPrediffer(UINT nID )
+{
+       SetPredifferByMenu(nID);
+       FlushAndRescan(true);
+}
+
+/**
+ * @brief Handler for all prediffer choices.
+ * Prediffer choises include ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO,
+ * ID_NO_PREDIFFER, & specific prediffers.
+ */
+void CMergeDoc::SetPredifferByMenu(UINT nID)
 {
-       OpenWithUnpackerDialog();
+       // update data for the radio button
+       m_CurrentPredifferID = nID;
+
+       if (nID == ID_NO_PREDIFFER)
+       {
+               // All flags are set correctly during the construction
+               PrediffingInfo infoPrediffer(false);
+               SetPrediffer(&infoPrediffer);
+               return;
+       }
+
+       String pluginName = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
+
+       // build a PrediffingInfo structure fom the ID
+       PrediffingInfo prediffer(pluginName);
+       
+       // update the prediffer and rescan
+       SetPrediffer(&prediffer);
 }
 
 void CMergeDoc::OnBnClickedFileEncoding()
@@ -3286,7 +3826,7 @@ void CMergeDoc::OnBnClickedPlugin()
        if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
                return;
        m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
-       OpenWithUnpackerDialog();
+       OnOpenWithUnpacker();
 }
 
 void CMergeDoc::OnBnClickedHexView()
@@ -3304,60 +3844,58 @@ void CMergeDoc::OnOK()
 void CMergeDoc::OnFileRecompareAsText()
 {
        m_bEnableTableEditing = false;
-       PackingInfo infoUnpacker;
-       SetUnpacker(&infoUnpacker);
        OnFileReload();
 }
 
 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
 {
-       pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode == PLUGIN_MODE::PLUGIN_BUILTIN_XML ||
-               m_ptBuf[0]->GetTableEditing());
+       pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
 }
 
 void CMergeDoc::OnFileRecompareAsTable()
 {
+       if (m_ptBuf[0]->GetTableEditing())
+       {
+               COpenTableDlg dlg;
+               if (dlg.DoModal() != IDOK)
+                       return;
+               m_pTablePropsPrepared.reset(new TableProps{ true, strutils::from_charstr(dlg.m_sDelimiterChar), strutils::from_charstr(dlg.m_sQuoteChar), dlg.m_bAllowNewlinesInQuotes });
+       }
        m_bEnableTableEditing = true;
-       PackingInfo infoUnpacker;
-       SetUnpacker(&infoUnpacker);
        OnFileReload();
 }
 
 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
 {
-       pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
-}
-
-void CMergeDoc::OnFileRecompareAsXML()
-{
-       m_bEnableTableEditing = false;
-       PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_BUILTIN_XML);
-       SetUnpacker(&infoUnpacker);
-       OnFileReload();
-}
-
-void CMergeDoc::OnUpdateFileRecompareAsXML(CCmdUI *pCmdUI)
-{
-       pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode != PLUGIN_MODE::PLUGIN_BUILTIN_XML);
+       pCmdUI->Enable(true);
 }
 
 void CMergeDoc::OnFileRecompareAs(UINT nID)
 {
+       if (!PromptAndSaveIfNeeded(true))
+               return;
+       
        DWORD dwFlags[3] = { 0 };
-       FileLocation fileloc[3];
+       PathContext paths = m_filePaths;
+       String strDesc[3];
+       int nBuffers = m_nBuffers;
+       PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
+
        for (int pane = 0; pane < m_nBuffers; pane++)
        {
-               fileloc[pane].setPath(m_filePaths[pane]);
                dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
+               strDesc[pane] = m_strDesc[pane];
        }
-       if (m_pEncodingErrorBar!=nullptr && m_pEncodingErrorBar->IsWindowVisible())
-               m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
-       if (nID == ID_MERGE_COMPARE_HEX)
-               GetMainFrame()->ShowHexMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
-       else
-               GetMainFrame()->ShowImgMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
-       GetParentFrame()->ShowWindow(SW_RESTORE);
-       GetParentFrame()->DestroyWindow();
+       if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
+       {
+               infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
+               nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
+               nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -static_cast<int>(nID);
+       }
+
+       if (GetMainFrame()->DoFileOrFolderOpen(&m_filePaths, dwFlags, strDesc, _T(""),
+           GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, &infoUnpacker, nullptr, nID))
+               GetParentFrame()->DestroyWindow();
 }
 
 // Return file extension either from file name 
@@ -3392,7 +3930,6 @@ bool CMergeDoc::GenerateReport(const String& sFileName) const
        }
 
        file.SetCodepage(ucr::CP_UTF_8);
-
        CString headerText =
                _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
                _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
@@ -3411,13 +3948,27 @@ bool CMergeDoc::GenerateReport(const String& sFileName) const
                _T("</style>\n")
                _T("</head>\n")
                _T("<body>\n")
-               _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
-               _T("<thead>\n")
-               _T("<tr>\n");
+               _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n");
        String header = 
                strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
        file.WriteString(header);
 
+       file.WriteString(_T("<colgroup>\n"));
+       double marginWidth = m_pView[0][0]->GetViewLineNumbers() ? 
+               strutils::to_str(m_pView[0][0]->GetLineCount()).length() / 1.5 + 0.5 : 0.5;
+       for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
+       {
+               String data = strutils::format(
+                       _T("<col style=\"width: %.1fem;\" />\n")
+                       _T("<col style=\"width: calc(100vw / %d - %.1fem);\" />\n"),
+                               marginWidth, m_nBuffers, marginWidth);
+               file.WriteString(data);
+       }
+       file.WriteString(
+               _T("</colgroup>\n")
+               _T("<thead>\n")
+               _T("<tr>\n"));
+       
        // Get paths
        // If archive, use archive path + folder + filename inside archive
        // If desc text given, use it
@@ -3425,7 +3976,12 @@ bool CMergeDoc::GenerateReport(const String& sFileName) const
        if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
        {
                for (int i = 0; i < paths.GetSize(); i++)
-                       m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
+               {
+                       if (!m_strDesc[i].empty())
+                               paths[i] = m_strDesc[i];
+                       else
+                               m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
+               }
        }
        else
        {
@@ -3440,8 +3996,7 @@ bool CMergeDoc::GenerateReport(const String& sFileName) const
        int nBuffer;
        for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
        {
-               String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
-                       (double)100 / m_nBuffers);
+               String data = _T("<th colspan=\"2\" class=\"title\">");
                file.WriteString(data);
                file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
                file.WriteString(_T("</th>\n"));
@@ -3583,7 +4138,7 @@ void CMergeDoc::OnToolsGeneratePatch()
  */
 void CMergeDoc::AddSyncPoint()
 {
-       int nLine[3];
+       int nLine[3]{};
        for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
        {
                int tmp = m_pView[0][nBuffer]->GetCursorPos().y;