OSDN Git Service

a0bfb4c9b64e34b582e3a1bb732d26c50d414a43
[winmerge-jp/winmerge-jp.git] / Src / MergeDoc.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  MergeDoc.cpp
9  *
10  * @brief Implementation file for CMergeDoc
11  *
12  */
13
14 #include "StdAfx.h"
15 #include "MergeDoc.h"
16 #include <io.h>
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
19 #include "Merge.h"
20 #include "MainFrm.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
26 #include "DirDoc.h"
27 #include "files.h"
28 #include "FileTransform.h"
29 #include "Plugins.h"
30 #include "unicoder.h"
31 #include "UniFile.h"
32 #include "OptionsDef.h"
33 #include "DiffFileInfo.h"
34 #include "SaveClosingDlg.h"
35 #include "OpenTableDlg.h"
36 #include "DiffList.h"
37 #include "paths.h"
38 #include "OptionsMgr.h"
39 #include "OptionsDiffOptions.h"
40 #include "MergeLineFlags.h"
41 #include "FileOrFolderSelect.h"
42 #include "LineFiltersList.h"
43 #include "SubstitutionFiltersList.h"
44 #include "TempFile.h"
45 #include "codepage_detect.h"
46 #include "SelectPluginDlg.h"
47 #include "EncodingErrorBar.h"
48 #include "MergeCmdLineInfo.h"
49 #include "TFile.h"
50 #include "Constants.h"
51 #include "Merge7zFormatMergePluginImpl.h"
52 #include "7zCommon.h"
53 #include "PatchTool.h"
54 #include "charsets.h"
55 #include "markdown.h"
56 #include "stringdiffs.h"
57
58 #ifdef _DEBUG
59 #define new DEBUG_NEW
60 #endif
61
62 using std::swap;
63
64 int CMergeDoc::m_nBuffersTemp = 2;
65
66 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
67
68 /////////////////////////////////////////////////////////////////////////////
69 // CMergeDoc
70
71 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
72
73 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
74         //{{AFX_MSG_MAP(CMergeDoc)
75         ON_COMMAND(ID_FILE_SAVE, OnFileSave)
76         ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
77         ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
78         ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
79         ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
80         ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
81         ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
82         ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
83         ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
84         ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
85         ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
86         ON_COMMAND(ID_RESCAN, OnFileReload)
87         ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
88         ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
89         ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
90         ON_COMMAND(ID_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
91         ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
92         ON_COMMAND_RANGE(ID_NO_PREDIFFER, ID_NO_PREDIFFER, OnPrediffer)
93         ON_COMMAND_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnPrediffer)
94         ON_UPDATE_COMMAND_UI(ID_NO_PREDIFFER, OnUpdatePrediffer)
95         ON_UPDATE_COMMAND_UI_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnUpdatePrediffer)
96         ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
97         ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
98         ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
99         ON_COMMAND(IDOK, OnOK)
100         ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
101         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
102         ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
103         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
104         ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
105         ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
106         ON_COMMAND(ID_SWAPPANES_SWAP12, (OnViewSwapPanes<0, 1>))
107         ON_COMMAND(ID_SWAPPANES_SWAP23, (OnViewSwapPanes<1, 2>))
108         ON_COMMAND(ID_SWAPPANES_SWAP13, (OnViewSwapPanes<0, 2>))
109         ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
110         //}}AFX_MSG_MAP
111 END_MESSAGE_MAP()
112
113 /////////////////////////////////////////////////////////////////////////////
114 // CMergeDoc construction/destruction
115
116 /**
117  * @brief Constructor.
118  */
119 CMergeDoc::CMergeDoc()
120 : m_bEnableRescan(true)
121 , m_nCurDiff(-1)
122 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
123 , m_pDirDoc(nullptr)
124 , m_bMixedEol(false)
125 , m_pEncodingErrorBar(nullptr)
126 , m_bHasSyncPoints(false)
127 , m_bAutoMerged(false)
128 , m_nGroups(0)
129 , m_pView{nullptr}
130 , m_bAutomaticRescan(false)
131 , m_CurrentPredifferID(0)
132 {
133         DIFFOPTIONS options = {0};
134
135         m_nBuffers = m_nBuffersTemp;
136         m_filePaths.SetSize(m_nBuffers);
137
138         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
139         {
140                 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
141                 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
142                 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
143                 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
144                 m_bEditAfterRescan[nBuffer] = false;
145         }
146
147         m_bEnableRescan = true;
148         m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
149
150         // COleDateTime m_LastRescan
151         curUndo = undoTgt.begin();
152         m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
153         m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
154
155         m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
156         Options::DiffOptions::Load(GetOptionsMgr(), options);
157
158         m_diffWrapper.SetOptions(&options);
159         m_diffWrapper.SetPrediffer(nullptr);
160 }
161
162 /**
163  * @brief Destructor.
164  *
165  * Informs associated dirdoc that mergedoc is closing.
166  */
167 CMergeDoc::~CMergeDoc()
168 {       
169         if (m_pDirDoc != nullptr)
170         {
171                 m_pDirDoc->MergeDocClosing(this);
172                 m_pDirDoc = nullptr;
173         }
174 }
175
176 /**
177  * @brief Deleted data associated with doc before closing.
178  */
179 void CMergeDoc::DeleteContents ()
180 {
181         CDocument::DeleteContents ();
182         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
183         {
184                 m_ptBuf[nBuffer]->FreeAll ();
185                 m_tempFiles[nBuffer].Delete();
186         }
187 }
188
189 /**
190  * @brief Called when new document is created.
191  *
192  * Initialises buffers.
193  */
194 BOOL CMergeDoc::OnNewDocument()
195 {
196         if (!CDocument::OnNewDocument())
197                 return false;
198
199         SetTitle(_("File Comparison").c_str());
200         
201         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
202                 m_ptBuf[nBuffer]->InitNew ();
203         return true;
204 }
205
206 /**
207  * @brief Return active merge edit view (or left one if neither active)
208  */
209 CMergeEditView * CMergeDoc::GetActiveMergeView()
210 {
211         CView * pActiveView = GetParentFrame()->GetActiveView();
212         CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
213         if (pMergeEditView == nullptr)
214                 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
215         return pMergeEditView;
216 }
217
218 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
219 {
220         return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
221 }
222
223 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
224 {
225         if (infoNewHandler != nullptr)
226         {
227                 m_infoUnpacker = *infoNewHandler;
228         }
229 }
230
231 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
232 {
233         m_diffWrapper.SetPrediffer(infoPrediffer);
234 }
235
236 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
237 {
238         m_diffWrapper.GetPrediffer(infoPrediffer);
239 }
240
241 const PrediffingInfo* CMergeDoc::GetPrediffer() const
242 {
243         static PrediffingInfo infoPrediffer;
244         m_diffWrapper.GetPrediffer(&infoPrediffer);
245         return &infoPrediffer;
246 }
247
248 /////////////////////////////////////////////////////////////////////////////
249 // CMergeDoc serialization
250
251 void CMergeDoc::Serialize(CArchive& ar)
252 {
253         ASSERT(false); // we do not use CDocument serialization
254 }
255
256 /////////////////////////////////////////////////////////////////////////////
257 // CMergeDoc commands
258
259 /**
260  * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
261  *
262  * @note 
263  * original file is Ansi : 
264  *   buffer  -> save as Ansi -> Ansi plugins -> diffutils
265  * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
266  *   buffer  -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
267  * (the plugins are optional, not the conversion)
268  * @todo Show SaveToFile() errors?
269  */
270 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
271 {
272         // and we don't repack the file
273         PackingInfo tempPacker(false);
274
275         // write buffer out to temporary file
276         String sError;
277         int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
278                 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
279 }
280
281 /**
282  * @brief Save files to temp files & compare again.
283  *
284  * @param bBinary [in,out] [in] If true, compare two binary files
285  * [out] If true binary file was detected.
286  * @param bIdentical [out] If true files were identical
287  * @param bForced [in] If true, suppressing is ignored and rescan
288  * is done always
289  * @return Tells if rescan was successfully, was suppressed, or
290  * error happened
291  * If this code is OK, Rescan has detached the views temporarily
292  * (positions of cursors have been lost)
293  * @note Rescan() ALWAYS compares temp files. Actual user files are not
294  * touched by Rescan().
295  * @sa CDiffWrapper::RunFileDiff()
296  */
297 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
298                 bool bForced /* =false */)
299 {
300         DIFFOPTIONS diffOptions = {0};
301         DiffFileInfo fileInfo;
302         bool diffSuccess = false;
303         int nResult = RESCAN_OK;
304         FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
305         int nBuffer;
306
307         if (!bForced)
308         {
309                 if (!m_bEnableRescan)
310                         return RESCAN_SUPPRESSED;
311         }
312
313         ClearWordDiffCache();
314
315         if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
316         {
317                 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
318         }
319         else
320         {
321                 m_diffWrapper.SetFilterList(_T(""));
322         }
323
324         if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
325         {
326                 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
327         }
328         else
329         {
330                 m_diffWrapper.SetSubstitutionList(nullptr);
331         }
332
333         if (GetView(0, 0)->m_CurSourceDef->type != 0)
334                 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
335         else
336                 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
337
338         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
339         {
340                 // Check if files have been modified since last rescan
341                 // Ignore checking in case of scratchpads (empty filenames)
342                 if (!m_filePaths[nBuffer].empty())
343                 {
344                         Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
345                                         fileInfo, false, nBuffer);
346                 }
347         }
348         m_LastRescan = COleDateTime::GetCurrentTime();
349
350         LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
351         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
352         {
353                 if (Changed[nBuffer] == FileChange::Removed)
354                 {
355                         String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
356                         ShowMessageBox(msg, MB_ICONWARNING);
357                         bool bSaveResult = false;
358                         bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
359                         if (!ok || !bSaveResult)
360                         {
361                                 return RESCAN_FILE_ERR;
362                         }
363                 }
364
365                 String temp = m_tempFiles[nBuffer].GetPath();
366                 if (temp.empty())
367                         temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
368                 if (temp.empty())
369                         return RESCAN_TEMP_ERR;
370         }
371
372         CheckFileChanged();
373
374         String tempPath = env::GetTemporaryPath();
375
376         // Set up DiffWrapper
377         m_diffWrapper.GetOptions(&diffOptions);
378
379         // Clear diff list
380         m_diffList.Clear();
381         m_nCurDiff = -1;
382         m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
383         // Clear moved lines lists
384         if (m_diffWrapper.GetDetectMovedBlocks())
385         {
386                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
387                         m_diffWrapper.GetMovedLines(nBuffer)->Clear();
388         }
389
390         // Set paths for diffing and run diff
391         m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
392         if (m_nBuffers < 3)
393                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
394         else
395                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
396         m_diffWrapper.SetCompareFiles(m_filePaths);
397
398         DIFFSTATUS status;
399
400         if (!HasSyncPoints())
401         {
402                 // Save text buffer to file
403                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
404                 {
405                         m_ptBuf[nBuffer]->SetTempPath(tempPath);
406                         SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
407                 }
408
409                 m_diffWrapper.SetCreateDiffList(&m_diffList);
410                 diffSuccess = m_diffWrapper.RunFileDiff();
411
412                 // Read diff-status
413                 m_diffWrapper.GetDiffStatus(&status);
414                 if (bBinary) // believe caller if we were told these are binaries
415                         status.bBinaries = true;
416         }
417         else
418         {
419                 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();   
420                 int nStartLine[3] = {0};
421                 int nLines[3], nRealLine[3];
422                 for (size_t i = 0; i <= syncpoints.size(); ++i)
423                 {
424                         // Save text buffer to file
425                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
426                         {
427                                 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
428                                 m_ptBuf[nBuffer]->SetTempPath(tempPath);
429                                 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(), 
430                                         nStartLine[nBuffer], nLines[nBuffer]);
431                         }
432                         DiffList templist;
433                         templist.Clear();
434                         m_diffWrapper.SetCreateDiffList(&templist);
435                         diffSuccess = m_diffWrapper.RunFileDiff();
436                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
437                                 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
438
439                         // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
440                         if (i == 0 && templist.GetSize() > 0)
441                                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
442                                         if (nStartLine[nBuffer] == 0)
443                                         {
444                                                 bool isEmptyFile = true;
445                                                 for (int j = 0; j < nLines[nBuffer]; j++)
446                                                 {
447                                                         if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
448                                                         {
449                                                                 isEmptyFile = false;
450                                                                 break;
451                                                         }
452                                                 }
453                                                 if (isEmptyFile)
454                                                 {
455                                                         DIFFRANGE di;
456                                                         templist.GetDiff(0, di);
457                                                         if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
458                                                         {
459                                                                 di.end[nBuffer] = -1;
460                                                                 templist.SetDiff(0, di);
461                                                         }
462                                                 }
463                                         }
464
465                         m_diffList.AppendDiffList(templist, nRealLine);
466                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
467                                 nStartLine[nBuffer] += nLines[nBuffer];
468
469                         // Read diff-status
470                         DIFFSTATUS status_part;
471                         m_diffWrapper.GetDiffStatus(&status_part);
472                         if (bBinary) // believe caller if we were told these are binaries
473                                 status.bBinaries = true;
474                         status.MergeStatus(status_part);
475                 }
476                 m_diffWrapper.SetCreateDiffList(&m_diffList);
477         }
478
479         // If one file has EOL before EOF and other not...
480         if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
481         {
482                 // ..last DIFFRANGE of file which has EOL must be
483                 // fixed to contain last line too
484                 int lineCount[3] = { 0,0,0 };
485                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
486                         lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
487                 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
488         }
489
490         // set identical/diff result as recorded by diffutils
491         identical = status.Identical;
492
493         // Determine errors and binary file compares
494         if (!diffSuccess)
495                 nResult = RESCAN_FILE_ERR;
496         else if (status.bBinaries)
497         {
498                 bBinary = true;
499         }
500         else
501         {
502                 // Now update views and buffers for ghost lines
503
504                 // Prevent displaying views during this update 
505                 // BTW, this solves the problem of double asserts
506                 // (during the display of an assert message box, a second assert in one of the 
507                 //  display functions happens, and hides the first assert)
508                 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
509
510                 // Remove blank lines and clear winmerge flags
511                 // this operation does not change the modified flag
512                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
513                         m_ptBuf[nBuffer]->prepareForRescan();
514
515                 // Divide diff blocks to match lines.
516                 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
517                         AdjustDiffBlocks();
518
519                 // Analyse diff-list (updating real line-numbers)
520                 // this operation does not change the modified flag
521                 PrimeTextBuffers();
522
523                 // Hide identical lines if diff-context is not 'All'
524                 HideLines();
525
526                 // Apply flags to lines that are trivial
527                 PrediffingInfo infoPrediffer;
528                 GetPrediffer(&infoPrediffer);
529                 if (!infoPrediffer.GetPluginPipeline().empty())
530                         FlagTrivialLines();
531                 
532                 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
533                 if (m_diffWrapper.GetDetectMovedBlocks())
534                         FlagMovedLines();
535                 
536                 // After PrimeTextBuffers() we know amount of real diffs
537                 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
538
539                 // Identical files are also updated
540                 if (!m_diffList.HasSignificantDiffs())
541                         identical = IDENTLEVEL::ALL;
542
543                 ForEachView([](auto& pView) {
544                         // just apply some options to the views
545                         pView->PrimeListWithFile();
546                         // Now buffers data are valid
547                         pView->ReAttachToBuffer();
548                 });
549                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
550                 {
551                         m_bEditAfterRescan[nBuffer] = false;
552                 }
553         }
554
555         if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
556                 identical == IDENTLEVEL::ALL &&
557                 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
558                         [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
559                 identical = IDENTLEVEL::NONE;
560
561         GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
562
563         return nResult;
564 }
565
566 void CMergeDoc::CheckFileChanged(void)
567 {
568         int nBuffer;
569         DiffFileInfo fileInfo;
570         FileChange FileChange[3];
571
572         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
573         {
574                 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
575                         false, nBuffer);
576
577                 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
578         }
579
580         bool bDoReload = false;
581         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
582         {
583                 if (FileChange[nBuffer] == FileChange::Changed)
584                 {
585                         String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge scanned it last time.\n\nDo you want to reload the file?"), m_filePaths[nBuffer]);
586                         if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
587                                 bDoReload = true;
588                         break;
589                 }
590         }
591         if (bDoReload)
592         {
593                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
594                 {
595                         if (FileChange[nBuffer] == FileChange::Changed)
596                         {
597                                 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
598                                 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
599                         }
600                 }
601         }
602 }
603
604 /** @brief Apply flags to lines that are trivial */
605 void CMergeDoc::FlagTrivialLines(void)
606 {
607         for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
608         {
609                 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
610                 {
611                         String str[3];
612                         for (int file = 0; file < m_nBuffers; ++file)
613                         {
614                                 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
615                                 str[file] = p ? p : _T("");
616                         }
617
618                         if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
619                         {
620                                 DIFFOPTIONS diffOptions = {0};
621                                 m_diffWrapper.GetOptions(&diffOptions);
622
623                                 // Make the call to stringdiffs, which does all the hard & tedious computations
624                                 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
625                                         !diffOptions.bIgnoreCase,
626                                         !diffOptions.bIgnoreEol,
627                                         diffOptions.nIgnoreWhitespace,
628                                         GetBreakType(), // whitespace only or include punctuation
629                                         GetByteColoringOption());
630                                 if (!worddiffs.empty())
631                                 {
632                                         for (int file = 0; file < m_nBuffers; ++file)
633                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
634                                 }
635                         }
636                 }
637         }
638 }
639
640 /** @brief Adjust all different lines that were detected as actually matching moved lines */
641 void CMergeDoc::FlagMovedLines(void)
642 {
643         int i;
644         MovedLines *pMovedLines;
645
646         pMovedLines = m_diffWrapper.GetMovedLines(0);
647         for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
648         {
649                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
650                 if (j != -1)
651                 {
652                         TRACE(_T("%d->%d\n"), i, j);
653                         ASSERT(j>=0);
654                         // We only flag lines that are already marked as being different
655                         int apparent = m_ptBuf[0]->ComputeApparentLine(i);
656                         if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
657                         {
658                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
659                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
660                                 {
661                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
662                                         if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
663                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
664                                 }
665                         }
666                 }
667         }
668
669         pMovedLines = m_diffWrapper.GetMovedLines(1);
670         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
671         {
672                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
673                 if (j != -1)
674                 {
675                         TRACE(_T("%d->%d\n"), i, j);
676                         ASSERT(j>=0);
677                         // We only flag lines that are already marked as being different
678                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
679                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
680                         {
681                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
682                                 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
683                                 {
684                                         int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
685                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
686                                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
687                                 }
688                         }
689                 }
690         }
691
692         if (m_nBuffers < 3)
693                 return;
694
695         pMovedLines = m_diffWrapper.GetMovedLines(1);
696         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
697         {
698                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
699                 if (j != -1)
700                 {
701                         TRACE(_T("%d->%d\n"), i, j);
702                         ASSERT(j>=0);
703                         // We only flag lines that are already marked as being different
704                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
705                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
706                         {
707                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
708                                 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
709                                 {
710                                         int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
711                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
712                                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
713                                 }
714                         }
715                 }
716         }
717
718         pMovedLines = m_diffWrapper.GetMovedLines(2);
719         for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
720         {
721                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
722                 if (j != -1)
723                 {
724                         TRACE(_T("%d->%d\n"), i, j);
725                         ASSERT(j>=0);
726                         // We only flag lines that are already marked as being different
727                         int apparent = m_ptBuf[2]->ComputeApparentLine(i);
728                         if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
729                         {
730                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
731                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
732                                 {
733                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
734                                         if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
735                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
736                                 }
737                         }
738                 }
739         }
740
741         // todo: Need to record actual moved information
742 }
743
744 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
745 {
746         if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
747         {
748                 GetParentFrame()->InitialUpdateFrame(this, true);
749                 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
750         }
751         return AfxMessageBox(sText.c_str(), nType, nIDHelp);
752 }
753
754 /**
755  * @brief Prints (error) message by rescan status.
756  *
757  * @param nRescanResult [in] Resultcocode from rescan().
758  * @param bIdentical [in] Were files identical?.
759  * @sa CMergeDoc::Rescan()
760  */
761 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
762 {
763         // Rescan was suppressed, there is no sensible status
764         if (nRescanResult == RESCAN_SUPPRESSED)
765                 return;
766
767         String s;
768
769         if (nRescanResult == RESCAN_FILE_ERR)
770         {
771                 s = _("An error occurred while comparing the files.");
772                 LogErrorString(s);
773                 ShowMessageBox(s, MB_ICONSTOP);
774                 return;
775         }
776
777         if (nRescanResult == RESCAN_TEMP_ERR)
778         {
779                 s = _("Temporary files could not be created. Check your temporary path settings.");
780                 LogErrorString(s);
781                 ShowMessageBox(s, MB_ICONSTOP);
782                 return;
783         }
784
785         // Files are not binaries, but they are identical
786         if (identical != IDENTLEVEL::NONE)
787         {
788                 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
789                         [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
790         }
791 }
792
793 bool CMergeDoc::Undo()
794 {
795         return false;
796 }
797
798 /**
799  * @brief An instance of RescanSuppress prevents rescan during its lifetime
800  * (or until its Clear method is called, which ends its effect).
801  */
802 class RescanSuppress
803 {
804 public:
805         explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
806         {
807                 m_bSuppress = true;
808                 m_bPrev = doc.m_bEnableRescan;
809                 m_doc.m_bEnableRescan = false;
810         }
811         void Clear() 
812         {
813                 if (m_bSuppress)
814                 {
815                         m_bSuppress = false;
816                         m_doc.m_bEnableRescan = m_bPrev;
817                 }
818         }
819         ~RescanSuppress()
820         {
821                 Clear();
822         }
823 private:
824         CMergeDoc & m_doc;
825         bool m_bPrev;
826         bool m_bSuppress;
827 };
828
829 /**
830  * @brief Copy all diffs from one side to side.
831  * @param [in] srcPane Source side from which diff is copied
832  * @param [in] dstPane Destination side
833  */
834 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
835 {
836         CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
837 }
838
839 /**
840  * @brief Copy range of diffs from one side to side.
841  * This function copies given range of differences from side to another.
842  * Ignored differences are skipped, and not copied.
843  * @param [in] srcPane Source side from which diff is copied
844  * @param [in] dstPane Destination side
845  * @param [in] firstDiff First diff copied (0-based index)
846  * @param [in] lastDiff Last diff copied (0-based index)
847  */
848 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
849 {
850 #ifdef _DEBUG
851         if (firstDiff > lastDiff)
852                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
853         if (firstDiff < 0)
854                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
855         if (lastDiff > m_diffList.GetSize() - 1)
856                 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
857 #endif
858
859         lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
860         firstDiff = max(0, firstDiff);
861         if (firstDiff > lastDiff)
862                 return;
863         
864         RescanSuppress suppressRescan(*this);
865
866         // Note we don't care about m_nDiffs count to become zero,
867         // because we don't rescan() so it does not change
868
869         SetCurrentDiff(lastDiff);
870
871         bool bGroupWithPrevious = false;
872         if (firstWordDiff <= 0 && lastWordDiff == -1)
873         {
874                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
875                         return; // sync failure
876         }
877         else
878         {
879                 if (!WordListCopy(srcPane, dstPane, lastDiff, 
880                         (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
881                         return; // sync failure
882         }
883
884         SetEditedAfterRescan(dstPane);
885
886         int nGroup = GetActiveMergeView()->m_nThisGroup;
887         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
888         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
889         CPoint currentPosSrc = pViewSrc->GetCursorPos();
890         currentPosSrc.x = 0;
891         CPoint currentPosDst = pViewDst->GetCursorPos();
892         currentPosDst.x = 0;
893
894         CPoint pt(0, 0);
895         pViewDst->SetCursorPos(pt);
896         pViewDst->SetNewSelection(pt, pt, false);
897         pViewDst->SetNewAnchor(pt);
898
899         // copy from bottom up is more efficient
900         for (int i = lastDiff - 1; i >= firstDiff; --i)
901         {
902                 if (m_diffList.IsDiffSignificant(i))
903                 {
904                         SetCurrentDiff(i);
905                         const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
906                         if (currentPosDst.y > pdi->dend)
907                         {
908                                 if (pdi->blank[dstPane] >= 0)
909                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
910                                 else if (pdi->blank[srcPane] >= 0)
911                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
912                         }                       
913                         // Group merge with previous (merge undo data to one action)
914                         bGroupWithPrevious = true;
915                         if (i > firstDiff || firstWordDiff <= 0)
916                         {
917                                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
918                                         break; // sync failure
919                         }
920                         else
921                         {
922                                 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
923                                         break; // sync failure
924                         }
925                 }
926         }
927
928         ForEachView(dstPane, [currentPosDst](auto& pView) {
929                 pView->SetCursorPos(currentPosDst);
930                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
931                 pView->SetNewAnchor(currentPosDst);
932         });
933
934         suppressRescan.Clear(); // done suppress Rescan
935         FlushAndRescan();
936 }
937
938 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
939         int firstLineDiff, int lastLineDiff)
940 {
941         lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
942         firstDiff = max(0, firstDiff);
943         if (firstDiff > lastDiff)
944                 return;
945         
946         RescanSuppress suppressRescan(*this);
947
948         bool bGroupWithPrevious = false;
949         if (firstLineDiff <= 0 && lastLineDiff == -1)
950         {
951                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
952                         return; // sync failure
953         }
954         else
955         {
956                 if (!PartialListCopy(srcPane, dstPane, lastDiff, 
957                         (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
958                         return; // sync failure
959         }
960
961
962         SetEditedAfterRescan(dstPane);
963
964         int nGroup = GetActiveMergeView()->m_nThisGroup;
965         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
966         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
967         CPoint currentPosSrc = pViewSrc->GetCursorPos();
968         currentPosSrc.x = 0;
969         CPoint currentPosDst = pViewDst->GetCursorPos();
970         currentPosDst.x = 0;
971
972         CPoint pt(0, 0);
973         pViewDst->SetCursorPos(pt);
974         pViewDst->SetNewSelection(pt, pt, false);
975         pViewDst->SetNewAnchor(pt);
976
977         // copy from bottom up is more efficient
978         for (int i = lastDiff - 1; i >= firstDiff; --i)
979         {
980                 if (m_diffList.IsDiffSignificant(i))
981                 {
982                         SetCurrentDiff(i);
983                         const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
984                         if (currentPosDst.y > pdi->dend)
985                         {
986                                 if (pdi->blank[dstPane] >= 0)
987                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
988                                 else if (pdi->blank[srcPane] >= 0)
989                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
990                         }                       
991                         // Group merge with previous (merge undo data to one action)
992                         bGroupWithPrevious = true;
993                         if (i > firstDiff || firstLineDiff <= 0)
994                         {
995                                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
996                                         break; // sync failure
997                         }
998                         else
999                         {
1000                                 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
1001                                         break; // sync failure
1002                         }
1003                 }
1004         }
1005
1006         ForEachView(dstPane, [currentPosDst](auto& pView) {
1007                 pView->SetCursorPos(currentPosDst);
1008                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1009                 pView->SetNewAnchor(currentPosDst);
1010         });
1011
1012         suppressRescan.Clear(); // done suppress Rescan
1013         FlushAndRescan();
1014 }
1015
1016 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1017
1018 template<class Type>
1019 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1020 {
1021         bool equal_all = middle == left && middle == right && left == right;
1022         if (equal_all)
1023                 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1024         bool conflict = middle != left && middle != right && left != right;
1025         if (conflict)
1026                 return std::pair<MergeResult, Type>(Conflict, left);
1027         switch (dstPane)
1028         {
1029         case 0:
1030                 if (left == right)
1031                         return std::pair<MergeResult, Type>(Merged, middle);
1032                 break;
1033         case 1:
1034                 if (left == middle)
1035                         return std::pair<MergeResult, Type>(Merged, right);
1036                 else
1037                         return std::pair<MergeResult, Type>(Merged, left);
1038                 break;
1039         case 2:
1040                 if (left == right)
1041                         return std::pair<MergeResult, Type>(Merged, middle);
1042                 break;
1043         }
1044         return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1045 }
1046
1047 /**
1048  * @brief Do auto-merge.
1049  * @param [in] dstPane Destination side
1050  */
1051 void CMergeDoc::DoAutoMerge(int dstPane)
1052 {
1053         const int lastDiff = m_diffList.GetSize() - 1;
1054         const int firstDiff = 0;
1055         bool bGroupWithPrevious = false;
1056         int autoMergedCount = 0;
1057         int unresolvedConflictCount = 0;
1058
1059         std::pair<MergeResult, FileTextEncoding> mergedEncoding = 
1060                 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1061         if (mergedEncoding.first == Merged)
1062         {
1063                 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1064                 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1065         }
1066         else if (mergedEncoding.first == Conflict)
1067                 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1068
1069         std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle = 
1070                 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1071         if (mergedEOLStyle.first == Merged)
1072         {
1073                 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1074                 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1075         }
1076         else if (mergedEOLStyle.first == Conflict)
1077                 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1078
1079         RescanSuppress suppressRescan(*this);
1080
1081         // Note we don't care about m_nDiffs count to become zero,
1082         // because we don't rescan() so it does not change
1083
1084         SetCurrentDiff(lastDiff);
1085
1086         SetEditedAfterRescan(dstPane);
1087
1088         int nGroup = GetActiveMergeView()->m_nThisGroup;
1089         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1090         CPoint currentPosDst = pViewDst->GetCursorPos();
1091         currentPosDst.x = 0;
1092
1093         CPoint pt(0, 0);
1094         pViewDst->SetCursorPos(pt);
1095         pViewDst->SetNewSelection(pt, pt, false);
1096         pViewDst->SetNewAnchor(pt);
1097
1098         // copy from bottom up is more efficient
1099         for (int i = lastDiff; i >= firstDiff; --i)
1100         {
1101                 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1102                 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1103                 if (srcPane != -1)
1104                 {
1105                         SetCurrentDiff(i);
1106                         if (currentPosDst.y > pdi->dend)
1107                         {
1108                                 if (pdi->blank[dstPane] >= 0)
1109                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1110                                 else if (pdi->blank[srcPane] >= 0)
1111                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1112                         }                       
1113                         // Group merge with previous (merge undo data to one action)
1114                         if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1115                                 break; // sync failure
1116                         if (!bGroupWithPrevious)
1117                                 bGroupWithPrevious = true;
1118                         ++autoMergedCount;
1119                 }
1120                 if (pdi->op == OP_DIFF)
1121                         ++unresolvedConflictCount;
1122         }
1123
1124         ForEachView(dstPane, [currentPosDst](auto& pView) {
1125                 pView->SetCursorPos(currentPosDst);
1126                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1127                 pView->SetNewAnchor(currentPosDst);
1128         });
1129
1130         suppressRescan.Clear(); // done suppress Rescan
1131         FlushAndRescan();
1132         UpdateHeaderPath(dstPane);
1133
1134         if (autoMergedCount > 0)
1135                 m_bAutoMerged = true;
1136
1137         // move to first conflict 
1138         const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1139         if (nDiff != -1)
1140                 pViewDst->SelectDiff(nDiff, true, false);
1141
1142         ShowMessageBox(
1143                 strutils::format_string2(
1144                         _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"), 
1145                         strutils::format(_T("%d"), autoMergedCount),
1146                         strutils::format(_T("%d"), unresolvedConflictCount)),
1147                 MB_ICONINFORMATION);
1148 }
1149
1150 /**
1151  * @brief Sanity check difference.
1152  *
1153  * Checks that lines in difference are inside difference in both files.
1154  * If file is edited, lines added or removed diff lines get out of sync and
1155  * merging fails miserably.
1156  *
1157  * @param [in] dr Difference to check.
1158  * @return true if difference lines match, false otherwise.
1159  */
1160 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1161 {
1162         const int cd_dbegin = dr.dbegin;
1163         const int cd_dend = dr.dend;
1164
1165         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1166         {
1167                 // Must ensure line number is in range before getting line flags
1168                 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1169                         return false;
1170
1171                 // Optimization - check last line first so we don't need to
1172                 // check whole diff for obvious cases
1173                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1174                 if (!(dwFlags & LF_WINMERGE_FLAGS))
1175                         return false;
1176         }
1177
1178         for (int line = cd_dbegin; line < cd_dend; line++)
1179         {
1180                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1181                 {
1182                         DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1183                         if (!(dwFlags & LF_WINMERGE_FLAGS))
1184                                 return false;
1185                 }
1186         }
1187         return true;
1188 }
1189
1190 /**
1191  * @brief Copy selected (=current) difference from from side to side.
1192  * @param [in] srcPane Source side from which diff is copied
1193  * @param [in] dstPane Destination side
1194  * @param [in] nDiff Diff to copy, if -1 function determines it.
1195  * @param [in] bGroupWithPrevious Adds diff to same undo group with
1196  * @return true if ok, false if sync failure & need to abort copy
1197  * previous action (allows one undo for copy all)
1198  */
1199 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1200                 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1201 {
1202         int nGroup = GetActiveMergeView()->m_nThisGroup;
1203         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1204         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1205         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1206
1207         // suppress Rescan during this method
1208         // (Not only do we not want to rescan a lot of times, but
1209         // it will wreck the line status array to rescan as we merge)
1210         RescanSuppress suppressRescan(*this);
1211
1212         // If diff-number not given, determine it from active view
1213         if (nDiff == -1)
1214         {
1215                 nDiff = GetCurrentDiff();
1216
1217                 // No current diff, but maybe cursor is in diff?
1218                 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1219                         pViewDst->IsCursorInDiff()))
1220                 {
1221                         // Find out diff under cursor
1222                         CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1223                         nDiff = m_diffList.LineToDiff(ptCursor.y);
1224                 }
1225         }
1226
1227         if (nDiff != -1)
1228         {
1229                 DIFFRANGE cd;
1230                 VERIFY(m_diffList.GetDiff(nDiff, cd));
1231                 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1232                 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1233                 bool bSrcWasMod = sbuf.IsModified();
1234                 const int cd_dbegin = cd.dbegin;
1235                 const int cd_dend = cd.dend;
1236                 const int cd_blank = cd.blank[srcPane];
1237                 bool bInSync = SanityCheckDiff(cd);
1238
1239                 if (!bInSync)
1240                 {
1241                         LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1242                         return false; // abort copying
1243                 }
1244
1245                 // If we remove whole diff from current view, we must fix cursor
1246                 // position first. Normally we would move to end of previous line,
1247                 // but we want to move to begin of that line for usability.
1248                 if (bUpdateView)
1249                 {
1250                         CPoint currentPos = pViewDst->GetCursorPos();
1251                         currentPos.x = 0;
1252                         if (currentPos.y > cd_dend)
1253                         {
1254                                 if (cd.blank[dstPane] >= 0)
1255                                         currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1256                                 else if (cd.blank[srcPane] >= 0)
1257                                         currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1258                         }
1259                         ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1260                 }
1261
1262                 // if the current diff contains missing lines, remove them from both sides
1263                 int limit = cd_dend;
1264
1265                 // curView is the view which is changed, so the opposite of the source view
1266                 dbuf.BeginUndoGroup(bGroupWithPrevious);
1267                 if (cd_blank>=0)
1268                 {
1269                         // text was missing, so delete rest of lines on both sides
1270                         // delete only on destination side since rescan will clear the other side
1271                         if (cd_dend + 1 < dbuf.GetLineCount())
1272                         {
1273                                 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1274                         }
1275                         else
1276                         {
1277                                 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1278                                 ASSERT(cd_blank > 0);
1279                                 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1280                         }
1281
1282                         limit=cd_blank-1;
1283                         dbuf.FlushUndoGroup(pSource);
1284                         dbuf.BeginUndoGroup(true);
1285                 }
1286
1287
1288                 // copy the selected text over
1289                 if (cd_dbegin <= limit)
1290                 {
1291                         // text exists on left side, so just replace
1292                         dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1293                         dbuf.FlushUndoGroup(pSource);
1294                         dbuf.BeginUndoGroup(true);
1295                 }
1296                 dbuf.FlushUndoGroup(pSource);
1297
1298                 // remove the diff
1299                 SetCurrentDiff(-1);
1300
1301                 // reset the mod status of the source view because we do make some
1302                 // changes, but none that concern the source text
1303                 sbuf.SetModified(bSrcWasMod);
1304         }
1305
1306         suppressRescan.Clear(); // done suppress Rescan
1307         FlushAndRescan();
1308         return true;
1309 }
1310
1311 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1312         bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1313 {
1314         int nGroup = GetActiveMergeView()->m_nThisGroup;
1315         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1316         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1317         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1318
1319         // suppress Rescan during this method
1320         // (Not only do we not want to rescan a lot of times, but
1321         // it will wreck the line status array to rescan as we merge)
1322         RescanSuppress suppressRescan(*this);
1323
1324         DIFFRANGE cd;
1325         VERIFY(m_diffList.GetDiff(nDiff, cd));
1326         CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1327         CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1328         bool bSrcWasMod = sbuf.IsModified();
1329         const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1330         const int cd_dend = cd.dend;
1331         const int cd_blank = cd.blank[srcPane];
1332         bool bInSync = SanityCheckDiff(cd);
1333
1334         if (!bInSync)
1335         {
1336                 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1337                 return false; // abort copying
1338         }
1339
1340         // If we remove whole diff from current view, we must fix cursor
1341         // position first. Normally we would move to end of previous line,
1342         // but we want to move to begin of that line for usability.
1343         if (bUpdateView)
1344         {
1345                 CPoint currentPos = pViewDst->GetCursorPos();
1346                 currentPos.x = 0;
1347                 if (currentPos.y > cd_dend)
1348                 {
1349                         if (cd.blank[dstPane] >= 0)
1350                                 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1351                         else if (cd.blank[srcPane] >= 0)
1352                                 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1353                 }
1354                 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1355         }
1356
1357         // if the current diff contains missing lines, remove them from both sides
1358         int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1359
1360         // curView is the view which is changed, so the opposite of the source view
1361         dbuf.BeginUndoGroup(bGroupWithPrevious);
1362         if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1363         {
1364                 // text was missing, so delete rest of lines on both sides
1365                 // delete only on destination side since rescan will clear the other side
1366                 if (limit+1 < dbuf.GetLineCount())
1367                 {
1368                         dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1369                 }
1370                 else
1371                 {
1372                         // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1373                         ASSERT(cd_dbegin > 0);
1374                         dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1375                 }
1376
1377                 limit = cd_dbegin-1;
1378                 dbuf.FlushUndoGroup(pSource);
1379                 dbuf.BeginUndoGroup(true);
1380         }
1381
1382         // copy the selected text over
1383         if (cd_dbegin <= limit)
1384         {
1385                 // text exists on left side, so just replace
1386                 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1387                 dbuf.FlushUndoGroup(pSource);
1388                 dbuf.BeginUndoGroup(true);
1389         }
1390         dbuf.FlushUndoGroup(pSource);
1391
1392         // remove the diff
1393         SetCurrentDiff(-1);
1394
1395         // reset the mod status of the source view because we do make some
1396         // changes, but none that concern the source text
1397         sbuf.SetModified(bSrcWasMod);
1398
1399         suppressRescan.Clear(); // done suppress Rescan
1400         FlushAndRescan();
1401         return true;
1402 }
1403
1404 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1405                 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1406 {
1407         int nGroup = GetActiveMergeView()->m_nThisGroup;
1408         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1409         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1410         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1411
1412         // suppress Rescan during this method
1413         // (Not only do we not want to rescan a lot of times, but
1414         // it will wreck the line status array to rescan as we merge)
1415         RescanSuppress suppressRescan(*this);
1416
1417         DIFFRANGE cd;
1418         VERIFY(m_diffList.GetDiff(nDiff, cd));
1419         CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1420         CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1421         bool bSrcWasMod = sbuf.IsModified();
1422         const int cd_dbegin = cd.dbegin;
1423         const int cd_dend = cd.dend;
1424         const int cd_blank = cd.blank[srcPane];
1425         bool bInSync = SanityCheckDiff(cd);
1426
1427         if (!bInSync)
1428         {
1429                 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1430                 return false; // abort copying
1431         }
1432
1433         std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1434
1435         if (worddiffs.empty())
1436                 return false;
1437
1438         if (cd.end[srcPane] < cd.begin[srcPane])
1439                 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1440
1441         if (firstWordDiff == -1)
1442                 firstWordDiff = 0;
1443         if (lastWordDiff == -1)
1444                 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1445
1446         // If we remove whole diff from current view, we must fix cursor
1447         // position first. Normally we would move to end of previous line,
1448         // but we want to move to begin of that line for usability.
1449         if (bUpdateView)
1450         {
1451                 CPoint currentPos = pViewDst->GetCursorPos();
1452                 currentPos.x = 0;
1453                 if (currentPos.y > cd_dend)
1454                 {
1455                         if (cd.blank[dstPane] >= 0)
1456                                 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1457                         else if (cd.blank[srcPane] >= 0)
1458                                 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1459                 }
1460                 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1461         }
1462
1463         // if the current diff contains missing lines, remove them from both sides
1464         int limit = cd_dend;
1465
1466         // curView is the view which is changed, so the opposite of the source view
1467         dbuf.BeginUndoGroup(bGroupWithPrevious);
1468
1469         CString srcText, dstText;
1470         CPoint ptDstStart, ptDstEnd;
1471         CPoint ptSrcStart, ptSrcEnd;
1472
1473         ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1474         ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1475         ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1476         ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1477         ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1478         ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1479         ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1480         ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1481
1482         std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1483         std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1484
1485         dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1486         sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1487
1488         nDstOffsets[0] = 0;
1489         for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1490                 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1491         nSrcOffsets[0] = 0;
1492         for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1493                 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1494
1495         for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1496         {
1497                 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1498                         continue;
1499                 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1500                 int srcEnd   = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1501                 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1502                 int dstEnd   = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1503                 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1504                         + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1505                         + dstText.Mid(dstEnd - ptDstStart.x);
1506         }
1507
1508         dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1509
1510         int endl,endc;
1511         dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1512
1513         dbuf.FlushUndoGroup(pSource);
1514
1515         // reset the mod status of the source view because we do make some
1516         // changes, but none that concern the source text
1517         sbuf.SetModified(bSrcWasMod);
1518
1519         suppressRescan.Clear(); // done suppress Rescan
1520         FlushAndRescan();
1521
1522         return true;
1523 }
1524
1525 /**
1526  * @brief Save file with new filename.
1527  *
1528  * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1529  * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1530  * normal filename fails, to let user choose another filename/location.
1531  * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1532  * using this function.
1533  * @param [in, out] strPath 
1534  * - [in] : Initial path shown to user
1535  * - [out] : Path to new filename if saving succeeds
1536  * @param [in, out] nSaveResult 
1537  * - [in] : Statuscode telling why we ended up here. Maybe the result of
1538  * previous save.
1539  * - [out] : Statuscode of this saving try
1540  * @param [in, out] sError Error string from lower level saving code
1541  * @param [in] nBuffer Buffer we are saving
1542  * @return false as long as the user is not satisfied. Calling function
1543  * should not continue until true is returned.
1544  * @sa CMergeDoc::DoSave()
1545  * @sa CMergeDoc::DoSaveAs()
1546  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1547  */
1548 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1549         int nBuffer, PackingInfo& infoTempUnpacker)
1550 {
1551         String s;
1552         String str;
1553         String strSavePath; // New path for next saving try
1554         String title;
1555         bool result = true;
1556         int answer = IDOK; // Set default we use for scratchpads
1557
1558         // We shouldn't get here if saving is succeed before
1559         ASSERT(nSaveResult != SAVE_DONE);
1560
1561         // Select message based on reason function called
1562         if (nSaveResult == SAVE_PACK_FAILED)
1563         {
1564                 str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
1565                 // replace the unpacker with a "do nothing" unpacker
1566                 infoTempUnpacker.Initialize(false);
1567         }
1568         else
1569         {
1570                 str = strutils::format_string2(_("Saving file failed.\n%1\n%2\nDo you want to:\n\t- use a different filename (Press OK)\n\t- abort the current operation (Press Cancel)?"), strPath, sError);
1571         }
1572
1573         // SAVE_NO_FILENAME is temporarily used for scratchpad.
1574         // So don't ask about saving in that case.
1575         if (nSaveResult != SAVE_NO_FILENAME)
1576                 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1577
1578         switch (answer)
1579         {
1580         case IDOK:
1581                 if (nBuffer == 0)
1582                         title = _("Save Left File As");
1583                 else if (nBuffer == m_nBuffers - 1)
1584                         title = _("Save Right File As");
1585                 else
1586                         title = _("Save Middle File As");
1587
1588                 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1589                 {
1590                         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1591                         strSavePath = s;
1592                         nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1593                                 infoTempUnpacker);
1594
1595                         if (nSaveResult == SAVE_DONE)
1596                         {
1597                                 // We are saving scratchpad (unnamed file)
1598                                 if (strPath.empty())
1599                                 {
1600                                         m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1601                                         m_strDesc[nBuffer].erase();
1602                                 }
1603                                         
1604                                 strPath = strSavePath;
1605                                 UpdateHeaderPath(nBuffer);
1606                         }
1607                         else
1608                                 result = false;
1609                 }
1610                 else
1611                         nSaveResult = SAVE_CANCELLED;
1612                 break;
1613
1614         case IDCANCEL:
1615                 nSaveResult = SAVE_CANCELLED;
1616                 break;
1617         }
1618         return result;
1619 }
1620
1621 /**
1622  * @brief Save file creating backups etc.
1623  *
1624  * Safe top-level file saving function. Checks validity of given path.
1625  * Creates backup file if wanted to. And if saving to given path fails,
1626  * allows user to select new location/name for file.
1627  * @param [in] szPath Path where to save including filename. Can be
1628  * empty/`nullptr` if new file is created (scratchpad) without filename.
1629  * @param [out] bSaveSuccess Will contain information about save success with
1630  * the original name (to determine if file statuses should be changed)
1631  * @param [in] nBuffer Index (0-based) of buffer to save
1632  * @return Tells if caller can continue (no errors happened)
1633  * @note Return value does not tell if SAVING succeeded. Caller must
1634  * Check value of bSaveSuccess parameter after calling this function!
1635  * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1636  * to directory it points to. If m_strSaveAsPath contains filename,
1637  * that filename is used.
1638  * @sa CMergeDoc::TrySaveAs()
1639  * @sa CMainFrame::CheckSavePath()
1640  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1641  */
1642 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1643 {
1644         DiffFileInfo fileInfo;
1645         String strSavePath(szPath);
1646         FileChange fileChanged;
1647         bool bApplyToAll = false;       
1648         int nRetVal = -1;
1649
1650         fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1651         if (fileChanged == FileChange::Changed)
1652         {
1653                 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1654                 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1655                 {
1656                         bSaveSuccess = true;
1657                         return true;
1658                 }               
1659         }
1660
1661         // use a temp packer
1662         // first copy the m_infoUnpacker
1663         // if an error arises during packing, change and take a "do nothing" packer
1664         PackingInfo infoTempUnpacker = m_infoUnpacker;
1665
1666         bSaveSuccess = false;
1667         
1668         // Check third arg possibly given from command-line
1669         if (!theApp.m_strSaveAsPath.empty())
1670         {
1671                 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1672                 {
1673                         // third arg was a directory, so get append the filename
1674                         String sname;
1675                         paths::SplitFilename(szPath, 0, &sname, 0);
1676                         strSavePath = theApp.m_strSaveAsPath;
1677                         strSavePath = paths::ConcatPath(strSavePath, sname);
1678                 }
1679                 else
1680                         strSavePath = theApp.m_strSaveAsPath;   
1681         }
1682
1683         nRetVal = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
1684         if (nRetVal == IDCANCEL)
1685                 return false;
1686
1687         if (!CMergeApp::CreateBackup(false, strSavePath))
1688                 return false;
1689
1690         // false as long as the user is not satisfied
1691         // true if saving succeeds, even with another filename, or if the user cancels
1692         bool result = false;
1693         // the error code from the latest save operation, 
1694         // or SAVE_DONE when the save succeeds
1695         // TODO: Shall we return this code in addition to bSaveSuccess ?
1696         int nSaveErrorCode = SAVE_DONE;
1697         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1698
1699         // Assume empty filename means Scratchpad (unnamed file)
1700         // Todo: This is not needed? - buffer type check should be enough
1701         if (strSavePath.empty())
1702                 nSaveErrorCode = SAVE_NO_FILENAME;
1703
1704         // Handle unnamed buffers
1705         if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1706                 nSaveErrorCode = SAVE_NO_FILENAME;
1707
1708         String sError;
1709         if (nSaveErrorCode == SAVE_DONE)
1710                 // We have a filename, just try to save
1711                 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, infoTempUnpacker);
1712
1713         if (nSaveErrorCode != SAVE_DONE)
1714         {
1715                 // Saving failed, user may save to another location if wants to
1716                 do
1717                         result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1718                 while (!result);
1719         }
1720
1721         // Saving succeeded with given/selected filename
1722         if (nSaveErrorCode == SAVE_DONE)
1723         {
1724                 // Preserve file times if user wants to
1725                 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1726                 {
1727                         fileInfo.SetFile(strSavePath);
1728                         try
1729                         {
1730                                 TFile file(strSavePath);
1731                                 file.setLastModified(fileInfo.mtime);
1732                         }
1733                         catch (...)
1734                         {
1735                         }
1736                 }
1737
1738                 m_ptBuf[nBuffer]->SetModified(false);
1739                 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1740                 m_filePaths[nBuffer] = strSavePath;
1741                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1742                 UpdateHeaderPath(nBuffer);
1743                 bSaveSuccess = true;
1744                 result = true;
1745         }
1746         else if (nSaveErrorCode == SAVE_CANCELLED)
1747         {
1748                 // User cancelled current operation, lets do what user wanted to do
1749                 result = false;
1750         }
1751         return result;
1752 }
1753
1754 /**
1755  * @brief Save file with different filename.
1756  *
1757  * Safe top-level file saving function. Asks user to select filename
1758  * and path. Does not create backups.
1759  * @param [in] szPath Path where to save including filename. Can be
1760  * empty/`nullptr` if new file is created (scratchpad) without filename.
1761  * @param [out] bSaveSuccess Will contain information about save success with
1762  * the original name (to determine if file statuses should be changed)
1763  * @param [in] nBuffer Index (0-based) of buffer to save
1764  * @return Tells if caller can continue (no errors happened)
1765  * @note Return value does not tell if SAVING succeeded. Caller must
1766  * Check value of bSaveSuccess parameter after calling this function!
1767  * @sa CMergeDoc::TrySaveAs()
1768  * @sa CMainFrame::CheckSavePath()
1769  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1770  */
1771 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1772 {
1773         String strSavePath(szPath);
1774
1775         // use a temp packer
1776         // first copy the m_infoUnpacker
1777         // if an error arises during packing, change and take a "do nothing" packer
1778         PackingInfo infoTempUnpacker = m_infoUnpacker;
1779
1780         bSaveSuccess = false;
1781         // false as long as the user is not satisfied
1782         // true if saving succeeds, even with another filename, or if the user cancels
1783         bool result = false;
1784         // the error code from the latest save operation, 
1785         // or SAVE_DONE when the save succeeds
1786         // TODO: Shall we return this code in addition to bSaveSuccess ?
1787         int nSaveErrorCode = SAVE_DONE;
1788
1789         // Use SAVE_NO_FILENAME to prevent asking about error
1790         nSaveErrorCode = SAVE_NO_FILENAME;
1791
1792         // Loop until user succeeds saving or cancels
1793         String sError;
1794         do
1795                 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1796         while (!result);
1797
1798         // Saving succeeded with given/selected filename
1799         if (nSaveErrorCode == SAVE_DONE)
1800         {
1801                 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1802                 m_filePaths[nBuffer] = strSavePath;
1803                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1804                 UpdateHeaderPath(nBuffer);
1805                 bSaveSuccess = true;
1806                 result = true;
1807         }
1808         return result;
1809 }
1810
1811 /**
1812  * @brief Get left->right info for a moved line (apparent line number)
1813  */
1814 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1815 {
1816         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1817                 return -1;
1818
1819         int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1820         int realRightLine = -1;
1821         if (m_diffWrapper.GetDetectMovedBlocks())
1822         {
1823                 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1824                                 MovedLines::SIDE::RIGHT);
1825         }
1826         if (realRightLine != -1)
1827                 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1828         else
1829                 return -1;
1830 }
1831
1832 /**
1833  * @brief Get right->left info for a moved line (apparent line number)
1834  */
1835 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1836 {
1837         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1838                 return -1;
1839
1840         int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1841         int realLeftLine = -1;
1842         if (m_diffWrapper.GetDetectMovedBlocks())
1843         {
1844                 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1845                                 MovedLines::SIDE::LEFT);
1846         }
1847         if (realLeftLine != -1)
1848                 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1849         else
1850                 return -1;
1851 }
1852
1853 /**
1854  * @brief Save modified documents.
1855  * This function asks if user wants to save modified documents. We also
1856  * allow user to cancel the closing.
1857  *
1858  * There is a special trick avoiding showing two save-dialogs, as MFC framework
1859  * sometimes calls this function twice. We use static counter for these calls
1860  * and if we already have saving in progress (counter == 1) we skip the new
1861  * saving dialog.
1862  *
1863  * @return true if docs are closed, false if closing is cancelled.
1864  */
1865 BOOL CMergeDoc::SaveModified()
1866 {
1867         static int counter;
1868         ++counter;
1869         if (counter > 1)
1870                 return false;
1871
1872         if (PromptAndSaveIfNeeded(true))
1873         {
1874                 counter = 0;
1875                 return true;
1876         }
1877         else
1878         {
1879                 counter = 0;
1880                 return false;
1881         }
1882 }
1883
1884 /**
1885  * @brief Sets the current difference.
1886  * @param [in] nDiff Difference to set as current difference.
1887  */
1888 void CMergeDoc::SetCurrentDiff(int nDiff)
1889 {
1890         if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1891                 m_nCurDiff = nDiff;
1892         else
1893                 m_nCurDiff = -1;
1894 }
1895
1896 /**
1897  * @brief Take care of rescanning document.
1898  * 
1899  * Update view and restore cursor and scroll position after
1900  * rescanning document.
1901  * @param [in] bForced If true rescan cannot be suppressed
1902  */
1903 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1904 {
1905         // Ignore suppressing when forced rescan
1906         if (!bForced)
1907                 if (!m_bEnableRescan) return;
1908
1909         CWaitCursor waitstatus;
1910
1911         CMergeEditView *pActiveView = GetActiveMergeView();
1912
1913         // store cursors and hide caret
1914         ForEachView([](auto& pView) { pView->PushCursors(); });
1915         pActiveView->HideCursor();
1916
1917         bool bBinary = false;
1918         IDENTLEVEL identical = IDENTLEVEL::NONE;
1919         int nRescanResult = Rescan(bBinary, identical, bForced);
1920
1921         // restore cursors and caret
1922         ForEachView([](auto& pView) { pView->PopCursors(); });
1923         pActiveView->ShowCursor();
1924
1925         ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1926                 // because of ghostlines, m_nTopLine may differ just after Rescan
1927                 // scroll both views to the same top line
1928                 pView->UpdateSiblingScrollPos(false);
1929         });
1930         // make sure we see the cursor from the curent view
1931         pActiveView->EnsureVisible(pActiveView->GetCursorPos());
1932
1933         // Refresh display
1934         UpdateAllViews(nullptr);
1935
1936         // Show possible error after updating screen
1937         if (nRescanResult != RESCAN_SUPPRESSED)
1938                 ShowRescanError(nRescanResult, identical);
1939         m_LastRescan = COleDateTime::GetCurrentTime();
1940 }
1941
1942 /**
1943  * @brief Saves both files
1944  */
1945 void CMergeDoc::OnFileSave() 
1946 {
1947         // We will need to know if either of the originals actually changed
1948         // so we know whether to update the diff status
1949         bool bChangedOriginal = false;
1950
1951         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1952         {
1953                 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1954                 {
1955                         // (why we don't use return value of DoSave)
1956                         // DoSave will return true if it wrote to something successfully
1957                         // but we have to know if it overwrote the original file
1958                         bool bSaveOriginal = false;
1959                         DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1960                         if (bSaveOriginal)
1961                                 bChangedOriginal = true;
1962                 }
1963         }
1964
1965         // If either of the actual source files being compared was changed
1966         // we need to update status in the dir view
1967         if (bChangedOriginal)
1968         {
1969                 // If DirDoc contains diffs
1970                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1971                 {
1972                         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1973                         {
1974                                 if (m_bEditAfterRescan[nBuffer])
1975                                 {
1976                                         FlushAndRescan(false);
1977                                         break;
1978                                 }
1979                         }
1980
1981                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1982                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1983                                         m_nTrivialDiffs, bIdentical);
1984                 }
1985         }
1986 }
1987
1988 void CMergeDoc::DoFileSave(int nBuffer)
1989 {
1990         bool bSaveSuccess = false;
1991         bool bModified = false;
1992
1993         if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1994         {
1995                 bModified = true;
1996                 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1997         }
1998
1999         // If file were modified and saving succeeded,
2000         // update status on dir view
2001         if (bModified && bSaveSuccess)
2002         {
2003                 // If DirDoc contains compare results
2004                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2005                 {
2006                         for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
2007                         {
2008                                 if (m_bEditAfterRescan[nBuffer1])
2009                                 {
2010                                         FlushAndRescan(false);
2011                                         break;
2012                                 }
2013                         }
2014
2015                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2016                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2017                                         m_nTrivialDiffs, bIdentical);
2018                 }
2019         }
2020 }
2021
2022 /**
2023  * @brief Saves left-side file
2024  */
2025 void CMergeDoc::OnFileSaveLeft()
2026 {
2027         DoFileSave(0);
2028 }
2029
2030 /**
2031  * @brief Saves middle-side file
2032  */
2033 void CMergeDoc::OnFileSaveMiddle()
2034 {
2035         DoFileSave(1);
2036 }
2037
2038 /**
2039  * @brief Saves right-side file
2040  */
2041 void CMergeDoc::OnFileSaveRight()
2042 {
2043         DoFileSave(m_nBuffers - 1);
2044 }
2045
2046 /**
2047  * @brief Saves left-side file with name asked
2048  */
2049 void CMergeDoc::OnFileSaveAsLeft()
2050 {
2051         bool bSaveResult = false;
2052         DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
2053 }
2054
2055 /**
2056  * @brief Called when "Save middle (as...)" item is updated
2057  */
2058 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
2059 {
2060         pCmdUI->Enable(m_nBuffers == 3);
2061 }
2062
2063 /**
2064  * @brief Saves right-side file with name asked
2065  */
2066 void CMergeDoc::OnFileSaveAsMiddle()
2067 {
2068         bool bSaveResult = false;
2069         DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
2070 }
2071
2072 /**
2073  * @brief Saves right-side file with name asked
2074  */
2075 void CMergeDoc::OnFileSaveAsRight()
2076 {
2077         bool bSaveResult = false;
2078         DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
2079 }
2080
2081 /**
2082  * @brief Update diff-number pane text in file compare.
2083  * The diff number pane shows selected difference/amount of differences when
2084  * there is difference selected. If there is no difference selected, then
2085  * the panel shows amount of differences. If there are ignored differences,
2086  * those are not count into numbers.
2087  * @param [in] pCmdUI UI component to update.
2088  */
2089 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI) 
2090 {
2091         TCHAR sIdx[32] = { 0 };
2092         TCHAR sCnt[32] = { 0 };
2093         String s;
2094         const int nDiffs = m_diffList.GetSignificantDiffs();
2095         
2096         // Files are identical - show text "Identical"
2097         if (nDiffs <= 0)
2098                 s = _("Identical");
2099         
2100         // There are differences, but no selected diff
2101         // - show amount of diffs
2102         else if (GetCurrentDiff() < 0)
2103         {
2104                 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
2105                 _itot_s(nDiffs, sCnt, 10);
2106                 strutils::replace(s, _T("%1"), sCnt);
2107         }
2108         
2109         // There are differences and diff selected
2110         // - show diff number and amount of diffs
2111         else
2112         {
2113                 s = _("Difference %1 of %2");
2114                 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
2115                 _itot_s(signInd + 1, sIdx, 10);
2116                 strutils::replace(s, _T("%1"), sIdx);
2117                 _itot_s(nDiffs, sCnt, 10);
2118                 strutils::replace(s, _T("%2"), sCnt);
2119         }
2120         pCmdUI->SetText(s.c_str());
2121 }
2122
2123 /**
2124  * @brief Change number of diff context lines
2125  */
2126 void CMergeDoc::OnDiffContext(UINT nID)
2127 {
2128         switch (nID)
2129         {
2130         case ID_VIEW_DIFFCONTEXT_0:
2131                 m_nDiffContext = 0; break;
2132         case ID_VIEW_DIFFCONTEXT_1:
2133                 m_nDiffContext = 1; break;
2134         case ID_VIEW_DIFFCONTEXT_3:
2135                 m_nDiffContext = 3; break;
2136         case ID_VIEW_DIFFCONTEXT_5:
2137                 m_nDiffContext = 5; break;
2138         case ID_VIEW_DIFFCONTEXT_7:
2139                 m_nDiffContext = 7; break;
2140         case ID_VIEW_DIFFCONTEXT_9:
2141                 m_nDiffContext = 9; break;
2142         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2143                 m_nDiffContext = -m_nDiffContext - 1; break;
2144         case ID_VIEW_DIFFCONTEXT_ALL:
2145                 if (m_nDiffContext >= 0)
2146                         m_nDiffContext = -m_nDiffContext - 1;
2147                 break;
2148         case ID_VIEW_DIFFCONTEXT_INVERT:
2149                 m_bInvertDiffContext = !m_bInvertDiffContext;
2150                 break;
2151         }
2152         GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2153         GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2154         FlushAndRescan(true);
2155 }
2156
2157 /**
2158  * @brief Swap the positions of the two panes
2159  */
2160 template<int srcPane, int dstPane>
2161 void CMergeDoc::OnViewSwapPanes()
2162 {
2163         SwapFiles(srcPane, dstPane);
2164 }
2165
2166 /**
2167  * @brief Swap context enable for 3 file compares 
2168  */
2169 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2170 {
2171         pCmdUI->Enable(m_nBuffers > 2);
2172 }
2173
2174 /**
2175  * @brief Update number of diff context lines
2176  */
2177 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2178 {
2179         bool bCheck;
2180         switch (pCmdUI->m_nID)
2181         {
2182         case ID_VIEW_DIFFCONTEXT_0:
2183                 bCheck = (m_nDiffContext == 0); break;
2184         case ID_VIEW_DIFFCONTEXT_1:
2185                 bCheck = (m_nDiffContext == 1); break;
2186         case ID_VIEW_DIFFCONTEXT_3:
2187                 bCheck = (m_nDiffContext == 3); break;
2188         case ID_VIEW_DIFFCONTEXT_5:
2189                 bCheck = (m_nDiffContext == 5); break;
2190         case ID_VIEW_DIFFCONTEXT_7:
2191                 bCheck = (m_nDiffContext == 7); break;
2192         case ID_VIEW_DIFFCONTEXT_9:
2193                 bCheck = (m_nDiffContext == 9); break;
2194         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2195                 bCheck = false; break;
2196         case ID_VIEW_DIFFCONTEXT_INVERT:
2197                 bCheck = m_bInvertDiffContext; break;
2198         default:
2199                 bCheck = (m_nDiffContext < 0); break;
2200         }
2201         pCmdUI->SetCheck(bCheck);
2202         pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2203 }
2204
2205 /**
2206  * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2207  *
2208  * @note After PrimeTextBuffers(), all buffers should have the same length.
2209  */
2210 void CMergeDoc::PrimeTextBuffers()
2211 {
2212         SetCurrentDiff(-1);
2213         m_nTrivialDiffs = 0;
2214         int nDiff;
2215         int nDiffCount = m_diffList.GetSize();
2216         int file;
2217
2218         // walk the diff list and calculate numbers of extra lines to add
2219         int extras[3] = {0, 0, 0};   // extra lines added to each view
2220         m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2221
2222         // resize m_aLines once for each view
2223         UINT lcount[3] = {0, 0, 0};
2224         UINT lcountnew[3] = {0, 0, 0};
2225         UINT lcountmax = 0;
2226         
2227         for (file = 0; file < m_nBuffers; file++)
2228         {
2229                 lcount[file] = m_ptBuf[file]->GetLineCount();
2230                 lcountnew[file] = lcount[file] + extras[file];
2231                 lcountmax = max(lcountmax, lcountnew[file]);
2232         }
2233         for (file = 0; file < m_nBuffers; file++)
2234         {
2235                 m_ptBuf[file]->m_aLines.resize(lcountmax);
2236         }
2237
2238         // walk the diff list backward, move existing lines to proper place,
2239         // add ghost lines, and set flags
2240         for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2241         {
2242                 DIFFRANGE curDiff;
2243                 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2244
2245                 // move matched lines after curDiff
2246                 int nline[3] = { 0, 0, 0 };
2247                 for (file = 0; file < m_nBuffers; file++)
2248                         nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2249                 // Matched lines should really match...
2250                 // But matched lines after last diff may differ because of empty last line (see function's note)
2251                 if (nDiff < nDiffCount - 1)
2252                         ASSERT(nline[0] == nline[1]);
2253
2254                 int nmaxline = 0;
2255                 for (file = 0; file < m_nBuffers; file++)
2256                 {
2257                         // Move all lines after current diff down as far as needed
2258                         // for any ghost lines we're about to insert
2259                         m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2260                         lcountnew[file] -= nline[file];
2261                         lcount[file] -= nline[file];
2262                         // move unmatched lines and add ghost lines
2263                         nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2264                         nmaxline = max(nmaxline, nline[file]);
2265                 }
2266
2267                 for (file = 0; file < m_nBuffers; file++)
2268                 {
2269                         DWORD dflag = LF_GHOST;
2270                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2271                                 dflag |= LF_SNP;
2272                         m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2273                         int nextra = nmaxline - nline[file];
2274                         if (nextra > 0)
2275                         {
2276                                 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2277                                 for (int i = 1; i <= nextra; i++)
2278                                         m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2279                         }
2280                         lcountnew[file] -= nmaxline;
2281
2282                         lcount[file] -= nline[file];
2283
2284                 }
2285                 // set dbegin, dend, blank, and line flags
2286                 curDiff.dbegin = lcountnew[0];
2287
2288                 switch (curDiff.op)
2289                 {
2290                 case OP_TRIVIAL:
2291                         ++m_nTrivialDiffs;
2292                         [[fallthrough]];
2293                 case OP_DIFF:
2294                 case OP_1STONLY:
2295                 case OP_2NDONLY:
2296                 case OP_3RDONLY:
2297                         // set curdiff
2298                         {
2299                                 curDiff.dend = lcountnew[0]+nmaxline-1;
2300                                 for (file = 0; file < m_nBuffers; file++)
2301                                 {
2302                                         curDiff.blank[file] = -1;
2303                                         int nextra = nmaxline - nline[file];
2304                                         if (nmaxline > nline[file])
2305                                         {
2306                                                 // more lines on left, ghost lines on right side
2307                                                 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2308                                         }
2309                                 }
2310                         }
2311                         // flag lines
2312                         {
2313                                 for (file = 0; file < m_nBuffers; file++)
2314                                 {
2315                                         // left side
2316                                         int i;
2317                                         for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2318                                         {
2319                                                 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2320                                                 {
2321                                                         // set diff or trivial flag
2322                                                         DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2323                                                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2324                                                                 dflag |= LF_SNP;
2325                                                         m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2326                                                         m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2327                                                 }
2328                                                 else
2329                                                 {
2330                                                         // ghost lines are already inserted (and flagged)
2331                                                         // ghost lines opposite to trivial lines are ghost and trivial
2332                                                         if (curDiff.op == OP_TRIVIAL)
2333                                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2334                                                 }
2335                                         }
2336                                 }
2337                         }
2338                         break;
2339                 }           // switch (curDiff.op)
2340                 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2341         }             // for (nDiff = nDiffCount; nDiff-- > 0; )
2342
2343         m_diffList.ConstructSignificantChain();
2344
2345 #ifdef _DEBUG
2346         // Note: By this point all `m_ptBuf[]` buffers must have the same  
2347         //              number of line entries; eventual buffer processing typically
2348         //              uses the line count from `m_ptBuf[0]` for all buffer processing.
2349
2350         for (file = 0; file < m_nBuffers; file++)
2351         {
2352                 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2353         }
2354 #endif
2355
2356         for (file = 0; file < m_nBuffers; file++)
2357                 m_ptBuf[file]->FinishLoading();
2358 }
2359
2360 /**
2361  * @brief Checks if file has changed since last update (save or rescan).
2362  * @param [in] szPath File to check
2363  * @param [in] dfi Previous fileinfo of file
2364  * @param [in] bSave If true Compare to last save-info, else to rescan-info
2365  * @param [in] nBuffer Index (0-based) of buffer
2366  * @return true if file is changed.
2367  */
2368 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2369         bool bSave, int nBuffer)
2370 {
2371         DiffFileInfo *fileInfo = nullptr;
2372         bool bFileChanged = false;
2373         bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2374         int tolerance = 0;
2375         if (bIgnoreSmallDiff)
2376                 tolerance = SmallTimeDiff; // From MainFrm.h
2377
2378         if (bSave)
2379                 fileInfo = m_pSaveFileInfo[nBuffer].get();
2380         else
2381                 fileInfo = m_pRescanFileInfo[nBuffer].get();
2382
2383         // We assume file existed, so disappearing means removal
2384         if (!dfi.Update(szPath))
2385                 return FileChange::Removed;
2386
2387         int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2388         if (timeDiff < 0) timeDiff = -timeDiff;
2389         if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2390         {
2391                 bFileChanged = true;
2392         }
2393
2394         if (bFileChanged)
2395                 return FileChange::Changed;
2396         else
2397                 return FileChange::NoChange;
2398 }
2399
2400 void CMergeDoc::HideLines()
2401 {
2402         int nLine;
2403         int file;
2404
2405         if (m_nDiffContext < 0)
2406         {
2407                 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2408                 return;
2409         }
2410
2411         int nLineCount = 0x7fffffff;
2412         for (file = 0; file < m_nBuffers; file++)
2413         {
2414                 if (nLineCount > m_ptBuf[file]->GetLineCount())
2415                         nLineCount = m_ptBuf[file]->GetLineCount();
2416         }
2417
2418         for (nLine =  0; nLine < nLineCount;)
2419         {
2420                 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2421                 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2422                 {
2423                         for (file = 0; file < m_nBuffers; file++)
2424                                 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2425                         nLine++;
2426                 }
2427                 else
2428                 {
2429                         int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2430                         for (; nLine2 < nLine; nLine2++)
2431                         {
2432                                 for (file = 0; file < m_nBuffers; file++)
2433                                         m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2434                         }
2435                 
2436                         for (; nLine < nLineCount; nLine++)
2437                         {
2438                                 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2439                                 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2440                                         break;
2441                                 for (file = 0; file < m_nBuffers; file++)
2442                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2443                         }
2444
2445                         int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2446                         for (; nLine < nLineEnd2; nLine++)
2447                         {
2448                                 for (file = 0; file < m_nBuffers; file++)
2449                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2450                                 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2451                                 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2452                                         nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2453                         }
2454                 }
2455         }
2456
2457         ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2458 }
2459
2460 /**
2461  * @brief Asks and then saves modified files.
2462  *
2463  * This function saves modified files. Dialog is shown for user to select
2464  * modified file(s) one wants to save or discard changed. Cancelling of
2465  * save operation is allowed unless denied by parameter. After successfully
2466  * save operation file statuses are updated to directory compare.
2467  * @param [in] bAllowCancel If false "Cancel" button is disabled.
2468  * @return true if user selected "OK" so next operation can be
2469  * executed. If false user choosed "Cancel".
2470  * @note If filename is empty, we assume scratchpads are saved,
2471  * so instead of filename, description is shown.
2472  * @todo If we have filename and description for file, what should
2473  * we do after saving to different filename? Empty description?
2474  * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2475  */
2476 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2477 {
2478         bool bLModified = false, bMModified = false, bRModified = false;
2479         bool result = true;
2480         bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2481
2482         if (m_nBuffers == 3)
2483         {
2484                 bLModified = m_ptBuf[0]->IsModified();
2485                 bMModified = m_ptBuf[1]->IsModified();
2486                 bRModified = m_ptBuf[2]->IsModified();
2487         }
2488         else
2489         {
2490                 bLModified = m_ptBuf[0]->IsModified();
2491                 bRModified = m_ptBuf[1]->IsModified();
2492         }
2493         if (!bLModified && !bMModified && !bRModified)
2494                  return true;
2495
2496         SaveClosingDlg dlg;
2497         dlg.DoAskFor(bLModified, bMModified, bRModified);
2498         if (!bAllowCancel)
2499                 dlg.m_bDisableCancel = true;
2500         if (!m_filePaths.GetLeft().empty())
2501         {
2502                 if (theApp.m_strSaveAsPath.empty())
2503                         dlg.m_sLeftFile = m_filePaths.GetLeft();
2504                 else
2505                         dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2506         }
2507         else
2508                 dlg.m_sLeftFile = m_strDesc[0];
2509         if (m_nBuffers == 3)
2510         {
2511                 if (!m_filePaths.GetMiddle().empty())
2512                 {
2513                         if (theApp.m_strSaveAsPath.empty())
2514                                 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2515                         else
2516                                 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2517                 }
2518                 else
2519                         dlg.m_sMiddleFile = m_strDesc[1];
2520         }
2521         if (!m_filePaths.GetRight().empty())
2522         {
2523                 if (theApp.m_strSaveAsPath.empty())
2524                         dlg.m_sRightFile = m_filePaths.GetRight();
2525                 else
2526                         dlg.m_sRightFile = theApp.m_strSaveAsPath;
2527         }
2528         else
2529                 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2530
2531         if (dlg.DoModal() == IDOK)
2532         {
2533                 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2534                 {
2535                         if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2536                                 result = false;
2537                 }
2538
2539                 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2540                 {
2541                         if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2542                                 result = false;
2543                 }
2544
2545                 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2546                 {
2547                         if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2548                                 result = false;
2549                 }
2550         }
2551         else
2552         {       
2553                 result = false;
2554         }
2555
2556         // If file were modified and saving was successfull,
2557         // update status on dir view
2558         if ((bLModified && bLSaveSuccess) || 
2559              (bMModified && bMSaveSuccess) ||
2560                  (bRModified && bRSaveSuccess))
2561         {
2562                 // If directory compare has results
2563                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2564                 {
2565                         if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2566                                 FlushAndRescan(false);
2567
2568                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2569                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2570                                         m_nTrivialDiffs, bIdentical);
2571                 }
2572         }
2573
2574         return result;
2575 }
2576
2577 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2578 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2579 {
2580         // if we did not rescan during the request timeOut, Rescan
2581         // else we did Rescan after the request, so do nothing
2582         COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2583         if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2584                 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2585                 FlushAndRescan();
2586 }
2587
2588 /**
2589  * @brief We have two child views (left & right), so we keep pointers directly
2590  * at them (the MFC view list doesn't have them both)
2591  */
2592 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2593 {
2594
2595         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2596                 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2597         ++m_nGroups;
2598 }
2599
2600 void CMergeDoc::RemoveMergeViews(int nGroup)
2601 {
2602
2603         for (; nGroup < m_nGroups - 1; nGroup++)
2604         {
2605                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2606                 {
2607                         m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2608                         m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2609                 }
2610         }
2611         --m_nGroups;
2612 }
2613
2614 /**
2615  * @brief DirDoc gives us its identity just after it creates us
2616  */
2617 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2618 {
2619         ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2620         m_pDirDoc = pDirDoc;
2621 }
2622
2623 /**
2624  * @brief Return pointer to parent frame
2625  */
2626 CMergeEditFrame * CMergeDoc::GetParentFrame() 
2627 {
2628         return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame()); 
2629 }
2630
2631 /**
2632  * @brief DirDoc is closing
2633  */
2634 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2635 {
2636         ASSERT(m_pDirDoc == pDirDoc);
2637         m_pDirDoc = nullptr;
2638         // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2639 }
2640
2641 /**
2642  * @brief DirDoc commanding us to close
2643  */
2644 bool CMergeDoc::CloseNow()
2645 {
2646         // Allow user to cancel closing
2647         if (!PromptAndSaveIfNeeded(true))
2648                 return false;
2649
2650         GetParentFrame()->DestroyWindow();
2651         return true;
2652 }
2653
2654 /**
2655  * @brief Loads file to buffer and shows load-errors
2656  * @param [in] sFileName File to open
2657  * @param [in] nBuffer Index (0-based) of buffer to load
2658  * @param [out] readOnly whether file is read-only
2659  * @param [in] encoding encoding used
2660  * @return Tells if files were loaded successfully
2661  * @sa CMergeDoc::OpenDocs()
2662  **/
2663 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2664 {
2665         String sError;
2666         DWORD retVal = FileLoadResult::FRESULT_ERROR;
2667
2668         CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2669         m_filePaths[nBuffer] = sFileName;
2670
2671         CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2672         CString sOpenError;
2673         retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
2674                 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2675
2676         // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2677         // it left the pBuf in a valid (but empty) state via a call to InitNew
2678
2679         if (FileLoadResult::IsOkImpure(retVal))
2680         {
2681                 // File loaded, and multiple EOL types in this file
2682                 FileLoadResult::SetMainOk(retVal);
2683
2684                 // If mixed EOLs are not enabled, enable them for this doc.
2685                 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2686                 {
2687                         pBuf->SetMixedEOL(true);
2688                 }
2689         }
2690
2691         if (FileLoadResult::IsError(retVal))
2692         {
2693                 // Error from Unifile/system
2694                 if (!sOpenError.IsEmpty())
2695                         sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2696                 else
2697                         sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2698                 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2699         }
2700         else if (FileLoadResult::IsErrorUnpack(retVal))
2701         {
2702                 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2703                 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2704         }
2705         return retVal;
2706 }
2707
2708 /**
2709  * @brief Check if specified codepage number is valid for WinMerge Editor.
2710  * @param [in] cp Codepage number to check.
2711  * @return true if codepage is valid, false otherwise.
2712  */
2713 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2714 {
2715         if (cp == 0) // 0 is our signal value for invalid
2716                 return false;
2717         return GetEncodingNameFromCodePage(cp) != nullptr;
2718 }
2719
2720 /**
2721  * @brief Sanity check file's specified codepage.
2722  * This function checks if file's specified codepage is valid for WinMerge
2723  * editor and if not resets the codepage to default.
2724  * @param [in,out] fileinfo Class containing file's codepage.
2725  */
2726 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2727 {
2728         if (fileinfo.encoding.m_unicoding == ucr::NONE
2729                 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2730         {
2731                 int cp = ucr::getDefaultCodepage();
2732                 if (!IsValidCodepageForMergeEditor(cp))
2733                         cp = CP_ACP;
2734                 fileinfo.encoding.SetCodepage(cp);
2735         }
2736 }
2737
2738 /**
2739  * @brief Loads one file from disk and updates file infos.
2740  * @param [in] index Index of file in internal buffers.
2741  * @param [in] filename File's name.
2742  * @param [in] readOnly Is file read-only?
2743  * @param [in] encoding File's encoding.
2744  * @return One of FileLoadResult values.
2745  */
2746 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc, 
2747                 const FileTextEncoding & encoding)
2748 {
2749         DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2750         
2751         m_strDesc[index] = strDesc;
2752         if (!filename.empty())
2753         {
2754                 if (strDesc.empty())
2755                         m_nBufferType[index] = BUFFERTYPE::NORMAL;
2756                 else
2757                         m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2758                 m_pSaveFileInfo[index]->Update(filename);
2759                 m_pRescanFileInfo[index]->Update(filename);
2760
2761                 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2762                 if (FileLoadResult::IsLossy(loadSuccess))
2763                 {
2764                         // Determine the file encoding by looking at all the contents of the file, not just part of it
2765                         FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
2766                         if (encoding != encodingNew)
2767                         {
2768                                 m_ptBuf[index]->FreeAll();
2769                                 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
2770                         }
2771                 }
2772         }
2773         else
2774         {
2775                 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2776                 m_ptBuf[index]->InitNew();
2777                 m_ptBuf[index]->m_encoding = encoding;
2778                 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer 
2779                 loadSuccess = FileLoadResult::FRESULT_OK;
2780         }
2781         return loadSuccess;
2782 }
2783
2784 void CMergeDoc::SetTableProperties()
2785 {
2786         struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
2787         auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
2788         {
2789                 const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
2790                 FileFilterHelper filterCSV, filterTSV, filterDSV;
2791                 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2792                 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2793                 if (!csvFilePattern.empty())
2794                 {
2795                         filterCSV.UseMask(true);
2796                         filterCSV.SetMask(csvFilePattern);
2797                         if (filterCSV.includeFile(path))
2798                                 return { true, ',', quote, allowNewlineIQuotes };
2799                 }
2800                 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2801                 if (!tsvFilePattern.empty())
2802                 {
2803                         filterTSV.UseMask(true);
2804                         filterTSV.SetMask(tsvFilePattern);
2805                         if (filterTSV.includeFile(path))
2806                                 return { true, '\t', quote, allowNewlineIQuotes };
2807                 }
2808                 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2809                 if (!dsvFilePattern.empty())
2810                 {
2811                         filterDSV.UseMask(true);
2812                         filterDSV.SetMask(dsvFilePattern);
2813                         if (filterDSV.includeFile(path))
2814                                 return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
2815                 }
2816                 if (enableTableEditing.value_or(false))
2817                 {
2818                         COpenTableDlg dlg;
2819                         if (dlg.DoModal() == IDOK)
2820                                 return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
2821                 }
2822                 return { false, 0, 0, false };
2823         };
2824
2825         TableProps tableProps[3] = {};
2826         int nTableFileIndex = -1;
2827         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2828         {
2829                 if (nBuffer == 0 ||
2830                         paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2831                         tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2832                 else
2833                         tableProps[nBuffer] = tableProps[nBuffer - 1];
2834                 if (tableProps[nBuffer].istable)
2835                         nTableFileIndex = nBuffer;
2836         }
2837         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2838         {
2839                 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2840                 {
2841                         int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
2842                         m_ptBuf[nBuffer]->SetTableEditing(true);
2843                         m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
2844                         m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
2845                         m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
2846                         m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
2847                         m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
2848                 }
2849                 else
2850                 {
2851                         m_ptBuf[nBuffer]->SetTableEditing(false);
2852                 }
2853         }
2854 }
2855
2856 /**
2857  * @brief Loads files and does initial rescan.
2858  * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2859  * @param bRO [in] Is left/middle/right file read-only
2860  * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2861  * @todo Options are still read from CMainFrame, this will change
2862  * @sa CMainFrame::ShowTextMergeDoc()
2863  */
2864 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2865                 const bool bRO[], const String strDesc[])
2866 {
2867         IDENTLEVEL identical = IDENTLEVEL::NONE;
2868         int nRescanResult = RESCAN_OK;
2869         int nBuffer;
2870         FileLocation fileloc[3];
2871
2872         std::copy_n(ifileloc, 3, fileloc);
2873
2874         // Filter out invalid codepages, or editor will display all blank
2875         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2876                 SanityCheckCodepage(fileloc[nBuffer]);
2877
2878         // clear undo stack
2879         undoTgt.clear();
2880         curUndo = undoTgt.begin();
2881
2882         // Prevent displaying views during LoadFile
2883         // Note : attach buffer again only if both loads succeed
2884         m_strBothFilenames.erase();
2885
2886         ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2887
2888         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2889         {
2890                 // clear undo buffers
2891                 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2892
2893                 // free the buffers
2894                 m_ptBuf[nBuffer]->FreeAll ();
2895
2896                 // build the text being filtered, "|" separates files as it is forbidden in filenames
2897                 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2898         }
2899         m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2900
2901         // Load files
2902         DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR,  FileLoadResult::FRESULT_ERROR };
2903         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2904         {
2905                 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2906                         fileloc[nBuffer].encoding);
2907                 if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
2908                 {
2909                         CMergeEditFrame* pFrame = GetParentFrame();
2910                         if (pFrame != nullptr)
2911                         {
2912                                 // Use verify macro to trap possible error in debug.
2913                                 VERIFY(pFrame->DestroyWindow());
2914                         }
2915                         return false;
2916                 }
2917         }
2918
2919         SetTableProperties();
2920
2921         const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
2922
2923         // scratchpad : we don't call LoadFile, so
2924         // we need to initialize the unpacker as a "do nothing" one
2925         if (bFiltersEnabled)
2926         { 
2927                 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
2928                 {
2929                         m_infoUnpacker.Initialize(false);
2930                 }
2931         }
2932
2933         // Warn user if file load was lossy (bad encoding)
2934         int idres=0;
2935         int nLossyBuffers = 0;
2936         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2937         {
2938                 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2939                 {
2940                         // TODO: It would be nice to report how many lines were lossy
2941                         // we did calculate those numbers when we loaded the files, in the text stats
2942         
2943                         idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2944                         nLossyBuffers++;
2945                 }
2946         }
2947         if (nLossyBuffers > 1)
2948                 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2949         
2950         if (nLossyBuffers > 0)
2951         {
2952                 if (m_pEncodingErrorBar == nullptr)
2953                 {
2954                         m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2955                         m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2956                 }
2957                 m_pEncodingErrorBar->SetText(LoadResString(idres));
2958                 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2959         }
2960
2961         ForEachView([](auto& pView) {
2962                 // Now buffers data are valid
2963                 pView->AttachToBuffer();
2964                 // Currently there is only one set of syntax colors, which all documents & views share
2965                 pView->SetColorContext(theApp.GetMainSyntaxColors());
2966                 // Currently there is only one set of markers, which all documents & views share
2967                 pView->SetMarkersContext(theApp.GetMainMarkers());
2968         });
2969         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2970         {
2971                 // Set read-only statuses
2972                 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2973         }
2974
2975         // Check the EOL sensitivity option (do it before Rescan)
2976         DIFFOPTIONS diffOptions = {0};
2977         m_diffWrapper.GetOptions(&diffOptions);
2978         if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2979         {
2980                 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2981                         if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2982                                 break;
2983
2984                 if (nBuffer < m_nBuffers)
2985                 {
2986                         // Options and files not are not compatible :
2987                         // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
2988                         // All lines will differ, that is not very interesting and probably not wanted.
2989                         // Propose to turn off the option 'sensitive to EOL'
2990                         String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
2991                         if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
2992                         {
2993                                 diffOptions.bIgnoreEol = true;
2994                                 m_diffWrapper.SetOptions(&diffOptions);
2995
2996                                 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
2997                                 const int nFormerResult = dlg.GetFormerResult();
2998                                 if (nFormerResult != -1)
2999                                 {
3000                                         // "Don't ask this question again" checkbox is checked
3001                                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
3002                                 }
3003                         }
3004                 }
3005         }
3006
3007         // Define the prediffer
3008         PackingInfo * infoUnpacker = nullptr;
3009         PrediffingInfo * infoPrediffer = nullptr;
3010         if (bFiltersEnabled && m_pDirDoc != nullptr)
3011         {
3012                 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3013                 m_diffWrapper.SetPrediffer(infoPrediffer);
3014                 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3015         }
3016
3017         bool bBinary = false;
3018         nRescanResult = Rescan(bBinary, identical);
3019
3020         // Open filed if rescan succeed and files are not binaries
3021         if (nRescanResult == RESCAN_OK)
3022         {
3023                 // set the document types
3024                 // Warning : it is the first thing to do (must be done before UpdateView,
3025                 // or any function that calls UpdateView, like SelectDiff)
3026                 // Note: If option enabled, and another side type is not recognized,
3027                 // we use recognized type for unrecognized side too.
3028                 String sext[3];
3029                 bool bTyped[3];
3030                 int paneTyped = 0;
3031
3032                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3033                 {
3034                         sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
3035                         ForEachView(nBuffer, [&](auto& pView) {
3036                                 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3037                                 if (bTyped[nBuffer])
3038                                         paneTyped = nBuffer;
3039                         });
3040                 }
3041
3042                 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3043                 {
3044                         if (bTyped[0] != bTyped[nBuffer])
3045                                 break;
3046                 }
3047
3048                 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3049                 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3050                 {
3051                         if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3052                         {
3053                                 CString sFirstLine;
3054                                 m_ptBuf[0]->GetLine(0, sFirstLine);
3055                                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3056                                 {
3057                                         bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3058                                 }
3059                         }
3060                 }
3061
3062                 if (syntaxHLEnabled)
3063                 {
3064                         CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3065                         ForEachView([&bTyped, enuType](auto& pView) {
3066                                 if (!bTyped[pView->m_nThisPane])
3067                                         pView->SetTextType(enuType);
3068                         });
3069                 }
3070
3071                 int nNormalBuffer = 0;
3072                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3073                 {
3074                         // set the frame window header
3075                         UpdateHeaderPath(nBuffer);
3076
3077                         ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3078                         
3079                         if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3080                             (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3081                         {
3082                                 nNormalBuffer++;
3083                         }
3084                         
3085                 }
3086
3087                 // Inform user that files are identical
3088                 // Don't show message if new buffers created
3089                 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3090                 {
3091                         ShowRescanError(nRescanResult, identical);
3092                 }
3093
3094                 // Exit if files are identical should only work for the first
3095                 // comparison and must be disabled afterward.
3096                 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3097         }
3098         else
3099         {
3100                 // CMergeDoc::Rescan fails if files do not exist on both sides 
3101                 // or the really arcane case that the temp files couldn't be created, 
3102                 // which is too obscure to bother reporting if you can't write to 
3103                 // your temp directory, doing nothing is graceful enough for that).
3104                 ShowRescanError(nRescanResult, identical);
3105                 GetParentFrame()->DestroyWindow();
3106                 return false;
3107         }
3108
3109         // Force repaint of location pane to update it in case we had some warning
3110         // dialog visible and it got painted before files were loaded
3111         if (m_pView[0][0] != nullptr)
3112                 m_pView[0][0]->RepaintLocationPane();
3113
3114         return true;
3115 }
3116
3117 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex, bool bRealLine)
3118 {
3119         if (nPane < 0)
3120         {
3121                 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3122                 if (nPane < 0 || nPane >= m_nBuffers)
3123                         nPane = 0;
3124         }
3125         if (nLineIndex == -1)
3126         {
3127                 // scroll to first diff
3128                 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3129                         m_diffList.HasSignificantDiffs())
3130                 {
3131                         int nDiff = m_diffList.FirstSignificantDiff();
3132                         if (nDiff != -1)
3133                                 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3134                         m_pView[0][nPane]->SetActivePane();
3135                         return;
3136                 }
3137         }
3138         m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, bRealLine, nPane);
3139 }
3140
3141 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3142 {
3143         if (!PromptAndSaveIfNeeded(true))
3144                 return;
3145
3146         FileLocation fileloc[3];
3147         String strDesc[3];
3148         bool bRO[3];
3149         for (int pane = 0; pane < m_nBuffers; pane++)
3150         {
3151                 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3152                 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3153                 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3154                 fileloc[pane].setPath(m_filePaths[pane]);
3155         }
3156         std::copy_n(m_strDesc, m_nBuffers, strDesc);
3157
3158         strDesc[nBuffer] = _T("");
3159         fileloc[nBuffer].setPath(path);
3160         fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3161         
3162         if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3163                 MoveOnLoad(nBuffer, nLineIndex);
3164 }
3165
3166 /**
3167  * @brief Re-load a document.
3168  * This methods re-loads the file compare document. The re-loaded document is
3169  * one side of the file compare.
3170  * @param [in] index The document to re-load.
3171  * @return Open result code.
3172  */
3173 void CMergeDoc::RefreshOptions()
3174 {
3175         DIFFOPTIONS options = {0};
3176         
3177         m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3178
3179         m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3180         Options::DiffOptions::Load(GetOptionsMgr(), options);
3181
3182         m_diffWrapper.SetOptions(&options);
3183
3184         // Refresh view options
3185         ForEachView([](auto& pView) { pView->RefreshOptions(); });
3186 }
3187
3188 /**
3189  * @brief Write path and filename to headerbar
3190  * @note SetText() does not repaint unchanged text
3191  */
3192 void CMergeDoc::UpdateHeaderPath(int pane)
3193 {
3194         CMergeEditFrame *pf = GetParentFrame();
3195         ASSERT(pf != nullptr);
3196         String sText;
3197         bool bChanges = false;
3198
3199         if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3200                 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3201         {
3202                 sText = m_strDesc[pane];
3203         }
3204         else
3205         {
3206                 sText = m_filePaths[pane];
3207                 if (m_pDirDoc != nullptr)
3208                 {
3209                         m_pDirDoc->ApplyDisplayRoot(pane, sText);
3210                 }
3211         }
3212         bChanges = m_ptBuf[pane]->IsModified();
3213
3214         if (bChanges)
3215                 sText.insert(0, _T("* "));
3216
3217         pf->GetHeaderInterface()->SetText(pane, sText);
3218
3219         SetTitle(nullptr);
3220 }
3221
3222 /**
3223  * @brief Paint differently the headerbar of the active view
3224  */
3225 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3226 {
3227         CMergeEditFrame *pf = GetParentFrame();
3228         ASSERT(pf != nullptr);
3229         pf->GetHeaderInterface()->SetActive(pane, bActivate);
3230 }
3231
3232 /**
3233  * @brief Set detect/not detect Moved Blocks
3234  */
3235 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3236 {
3237         if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3238                 return;
3239
3240         GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3241         m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3242         FlushAndRescan();
3243 }
3244
3245 /**
3246  * @brief Check if given buffer has mixed EOL style.
3247  * @param [in] nBuffer Buffer to check.
3248  * @return true if buffer's EOL style is mixed, false otherwise.
3249  */
3250 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3251 {
3252         CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3253         return pBuf->IsMixedEOL();
3254 }
3255
3256 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3257 {
3258         m_bEditAfterRescan[nBuffer] = true;
3259 }
3260
3261 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3262 {
3263         if (nBuffer >= 0 && nBuffer < m_nBuffers)
3264                 return m_bEditAfterRescan[nBuffer];
3265
3266         for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3267         {
3268                 if (m_bEditAfterRescan[nBuffer])
3269                         return true;
3270         }
3271
3272         return false;
3273 }
3274
3275 /**
3276  * @brief Update document filenames to title
3277  */
3278 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3279 {
3280         PrediffingInfo infoPrediffer;
3281         GetPrediffer(&infoPrediffer);
3282         String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer);
3283         CDocument::SetTitle(sTitle.c_str());
3284 }
3285
3286 /**
3287  * @brief Update any resources necessary after a GUI language change
3288  */
3289 void CMergeDoc::UpdateResources()
3290 {
3291         if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3292                 m_strDesc[0] = _("Untitled left");
3293         if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3294                 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3295         if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3296                 m_strDesc[1] = _("Untitled middle");
3297         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3298                 UpdateHeaderPath(nBuffer);
3299
3300         GetParentFrame()->UpdateResources();
3301         ForEachView([](auto& pView) { pView->UpdateResources(); });
3302 }
3303
3304 // Return current word breaking break type setting (whitespace only or include punctuation)
3305 bool CMergeDoc::GetBreakType() const
3306 {
3307         bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3308         return breakType;
3309 }
3310
3311 // Return true to do line diff colors at the byte level (false to do them at word level)
3312 bool CMergeDoc::GetByteColoringOption() const
3313 {
3314         // color at byte level if 'break_on_words' option not set
3315         bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3316         return !breakWords;
3317 }
3318
3319 /// Swap files and update views
3320 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3321 {
3322         if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3323         {
3324                 // Swap views
3325                 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3326                 {
3327                         int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3328                         int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3329                         m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3330                         m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3331                 }
3332
3333
3334                 // Swap buffers and so on
3335                 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3336                 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3337                         std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3338                 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3339                 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3340                 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3341                 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3342                 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3343
3344                 m_filePaths.Swap(nFromIndex, nToIndex);
3345                 m_diffList.Swap(nFromIndex, nToIndex);
3346                 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3347                         swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3348
3349                 ClearWordDiffCache();
3350
3351                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3352                 {
3353                         m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3354                         for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3355                                 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3356
3357                         // Update views
3358                         UpdateHeaderPath(nBuffer);
3359                 }
3360                 GetParentFrame()->UpdateSplitter();
3361                 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3362
3363                 UpdateAllViews(nullptr);
3364         }
3365 }
3366
3367 /**
3368  * @brief Reloads the opened files
3369  */
3370 void CMergeDoc::OnFileReload()
3371 {
3372         if (!PromptAndSaveIfNeeded(true))
3373                 return;
3374         
3375         FileLocation fileloc[3];
3376         bool bRO[3];
3377         for (int pane = 0; pane < m_nBuffers; pane++)
3378         {
3379                 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3380                 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3381                 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3382                 fileloc[pane].setPath(m_filePaths[pane]);
3383         }
3384         CPoint pt = GetActiveMergeView()->GetCursorPos();
3385         if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3386                 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3387 }
3388
3389 /**
3390  * @brief Display encodings to user
3391  */
3392 void CMergeDoc::OnFileEncoding()
3393 {
3394         DoFileEncodingDialog();
3395 }
3396
3397 void CMergeDoc::OnOpenWithUnpacker()
3398 {
3399         CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
3400                 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3401                 CSelectPluginDlg::PluginType::Unpacker, false);
3402         if (dlg.DoModal() != IDOK)
3403                 return;
3404
3405         if (!PromptAndSaveIfNeeded(true))
3406                 return;
3407
3408         PackingInfo infoUnpacker(dlg.GetPluginPipeline());
3409         PathContext paths = m_filePaths;
3410         DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
3411         String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
3412         int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3413         nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3414
3415         if (GetMainFrame()->DoFileOpen(nID, &paths, dwFlags, strDesc, _T(""), &infoUnpacker))
3416                 GetParentFrame()->DestroyWindow();
3417 }
3418
3419 void CMergeDoc::OnApplyPrediffer() 
3420 {
3421         PrediffingInfo prediffer;
3422         GetPrediffer(&prediffer);
3423         // let the user choose a handler
3424         CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
3425                 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3426                 CSelectPluginDlg::PluginType::Prediffer, false);
3427         if (dlg.DoModal() != IDOK)
3428                 return;
3429         prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
3430         SetPrediffer(&prediffer);
3431         m_CurrentPredifferID = -1;
3432         FlushAndRescan(true);
3433         SetTitle(nullptr);
3434 }
3435
3436 /**
3437  * @brief Create the dynamic submenu for prediffers
3438  *
3439  * @note The plugins are grouped in (suggested) and (not suggested)
3440  *       The IDs follow the order of GetAvailableScripts
3441  *       For example :
3442  *                              suggested 0         ID_1ST + 0 
3443  *                              suggested 1         ID_1ST + 2 
3444  *                              suggested 2         ID_1ST + 5 
3445  *                              not suggested 0     ID_1ST + 1 
3446  *                              not suggested 1     ID_1ST + 3 
3447  *                              not suggested 2     ID_1ST + 4 
3448  */
3449 HMENU CMergeDoc::createPrediffersSubmenu(HMENU hMenu)
3450 {
3451         // empty the menu
3452         int j = GetMenuItemCount(hMenu);
3453         while (j --)
3454                 DeleteMenu(hMenu, 0, MF_BYPOSITION);
3455
3456         // title
3457         AppendMenu(hMenu, MF_STRING, ID_NO_PREDIFFER, _("No prediffer (normal)").c_str());
3458         
3459         if (!GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED))
3460                 return hMenu;
3461
3462         m_CurrentPredifferID = -1;
3463
3464         // compute the m_CurrentPredifferID (to set the radio button)
3465         PrediffingInfo prediffer;
3466         GetPrediffer(&prediffer);
3467         if (prediffer.GetPluginPipeline().empty())
3468                 m_CurrentPredifferID = ID_NO_PREDIFFER;
3469
3470         // get the scriptlet files
3471         const auto& [ suggestedPlugins, allPlugins ]= FileTransform::CreatePluginMenuInfos(
3472                 m_strBothFilenames, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3473
3474         // build the menu : first part, suggested plugins
3475         // title
3476         AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3477         AppendMenu(hMenu, MF_STRING, ID_SUGGESTED_PLUGINS, _("Suggested plugins").c_str());
3478
3479         for (const auto& [caption, name, id, plugin ] : suggestedPlugins)
3480                 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3481
3482         // build the menu : second part, others plugins
3483         // title
3484         AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3485         AppendMenu(hMenu, MF_STRING, ID_NOT_SUGGESTED_PLUGINS, _("Other plugins").c_str());
3486
3487         String lastPluginName;
3488         String errorMessage;
3489         auto result = prediffer.ParsePluginPipeline(errorMessage);
3490         if (result.size() > 0)
3491                 lastPluginName = result.back().name;
3492         
3493         for (const auto& [processType, pluginAry] : allPlugins)
3494         {
3495                 for (const auto& [caption, name, id, plugin] : pluginAry)
3496                 {
3497                         if (!name.empty())
3498                         {
3499                                 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3500                                 if (lastPluginName == plugin->m_name)
3501                                         m_CurrentPredifferID = id;
3502                         }
3503                 }
3504         }
3505
3506         return hMenu;
3507 }
3508
3509 /**
3510  * @brief Called when an editor script item is updated
3511  */
3512 void CMergeDoc::OnUpdatePrediffer(CCmdUI* pCmdUI)
3513 {
3514         pCmdUI->Enable(true);
3515
3516         PrediffingInfo prediffer;
3517         GetPrediffer(&prediffer);
3518
3519         if (prediffer.GetPluginPipeline().find(_T("<Automatic>")) != String::npos)
3520         {
3521                 pCmdUI->SetRadio(false);
3522                 return;
3523         }
3524
3525         // Detect when CDiffWrapper::RunFileDiff has canceled a buggy prediffer
3526         if (prediffer.GetPluginPipeline().empty())
3527                 m_CurrentPredifferID = ID_NO_PREDIFFER;
3528
3529         pCmdUI->SetRadio(pCmdUI->m_nID == static_cast<UINT>(m_CurrentPredifferID));
3530 }
3531
3532 /**
3533  * @brief Handler for all prediffer choices, including ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO, ID_NO_PREDIFFER, & specific prediffers
3534  */
3535 void CMergeDoc::OnPrediffer(UINT nID )
3536 {
3537         SetPredifferByMenu(nID);
3538         FlushAndRescan(true);
3539         SetTitle(nullptr);
3540 }
3541
3542 /**
3543  * @brief Handler for all prediffer choices.
3544  * Prediffer choises include ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO,
3545  * ID_NO_PREDIFFER, & specific prediffers.
3546  */
3547 void CMergeDoc::SetPredifferByMenu(UINT nID)
3548 {
3549         // update data for the radio button
3550         m_CurrentPredifferID = nID;
3551
3552         if (nID == ID_NO_PREDIFFER)
3553         {
3554                 // All flags are set correctly during the construction
3555                 PrediffingInfo infoPrediffer(false);
3556                 SetPrediffer(&infoPrediffer);
3557                 return;
3558         }
3559
3560         String pluginName = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3561
3562         // build a PrediffingInfo structure fom the ID
3563         PrediffingInfo prediffer(pluginName);
3564         
3565         // update the prediffer and rescan
3566         SetPrediffer(&prediffer);
3567 }
3568
3569 void CMergeDoc::OnBnClickedFileEncoding()
3570 {
3571         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3572                 return;
3573         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3574         DoFileEncodingDialog();
3575 }
3576
3577 void CMergeDoc::OnBnClickedPlugin()
3578 {
3579         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3580                 return;
3581         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3582         OnOpenWithUnpacker();
3583 }
3584
3585 void CMergeDoc::OnBnClickedHexView()
3586 {
3587         OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3588 }
3589
3590 void CMergeDoc::OnOK()
3591 {
3592         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3593                 return;
3594         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3595 }
3596
3597 void CMergeDoc::OnFileRecompareAsText()
3598 {
3599         m_bEnableTableEditing = false;
3600         OnFileReload();
3601 }
3602
3603 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3604 {
3605         pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
3606 }
3607
3608 void CMergeDoc::OnFileRecompareAsTable()
3609 {
3610         m_bEnableTableEditing = true;
3611         OnFileReload();
3612 }
3613
3614 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3615 {
3616         pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3617 }
3618
3619 void CMergeDoc::OnFileRecompareAs(UINT nID)
3620 {
3621         if (!PromptAndSaveIfNeeded(true))
3622                 return;
3623         
3624         DWORD dwFlags[3] = { 0 };
3625         FileLocation fileloc[3];
3626         String strDesc[3];
3627         int nBuffers = m_nBuffers;
3628         CDirDoc *pDirDoc = m_pDirDoc->GetMainView() ? m_pDirDoc : 
3629                 static_cast<CDirDoc*>(theApp.m_pDirTemplate->CreateNewDocument());
3630         PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
3631
3632         for (int pane = 0; pane < m_nBuffers; pane++)
3633         {
3634                 fileloc[pane].setPath(m_filePaths[pane]);
3635                 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3636                 strDesc[pane] = m_strDesc[pane];
3637         }
3638         if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
3639         {
3640                 infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
3641                 nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3642                 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3643         }
3644
3645         if (GetMainFrame()->ShowMergeDoc(nID, pDirDoc, nBuffers, fileloc, dwFlags, strDesc, _T(""), &infoUnpacker))
3646                 GetParentFrame()->DestroyWindow();
3647 }
3648
3649 // Return file extension either from file name 
3650 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3651 {
3652         String sExt;
3653         paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3654         return sExt;
3655 }
3656
3657 /**
3658  * @brief Generate report from file compare results.
3659  */
3660 bool CMergeDoc::GenerateReport(const String& sFileName) const
3661 {
3662         // calculate HTML font size
3663         LOGFONT lf;
3664         CDC dc;
3665         dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3666         m_pView[0][0]->GetFont(lf);
3667         int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3668
3669         // create HTML report
3670         UniStdioFile file;
3671         if (!file.Open(sFileName, _T("wt")))
3672         {
3673                 String errMsg = GetSysError(GetLastError());
3674                 String msg = strutils::format_string1(
3675                         _("Error creating the report:\n%1"), errMsg);
3676                 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3677                 return false;
3678         }
3679
3680         file.SetCodepage(ucr::CP_UTF_8);
3681
3682         CString headerText =
3683                 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3684                 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3685                 _T("<html>\n")
3686                 _T("<head>\n")
3687                 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3688                 _T("<title>WinMerge File Compare Report</title>\n")
3689                 _T("<style type=\"text/css\">\n")
3690                 _T("<!--\n")
3691                 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3692                 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3693                 _T("tr { vertical-align: top; }\n")
3694                 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3695                 _T("%s")
3696                 _T("-->\n")
3697                 _T("</style>\n")
3698                 _T("</head>\n")
3699                 _T("<body>\n")
3700                 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3701                 _T("<thead>\n")
3702                 _T("<tr>\n");
3703         String header = 
3704                 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3705         file.WriteString(header);
3706
3707         // Get paths
3708         // If archive, use archive path + folder + filename inside archive
3709         // If desc text given, use it
3710         PathContext paths = m_filePaths;
3711         if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3712         {
3713                 for (int i = 0; i < paths.GetSize(); i++)
3714                         m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3715         }
3716         else
3717         {
3718                 for (int i = 0; i < paths.GetSize(); i++)
3719                 {
3720                         if (!m_strDesc[i].empty())
3721                                 paths[i] = m_strDesc[i];
3722                 }
3723         }
3724
3725         // titles
3726         int nBuffer;
3727         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3728         {
3729                 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3730                         (double)100 / m_nBuffers);
3731                 file.WriteString(data);
3732                 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3733                 file.WriteString(_T("</th>\n"));
3734         }
3735         file.WriteString(
3736                 _T("</tr>\n")
3737                 _T("</thead>\n")
3738                 _T("<tbody>\n"));
3739
3740         // write the body of the report
3741         int idx[3] = {0};
3742         int nLineCount[3] = {0};
3743         int nDiff = 0;
3744         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3745                 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3746
3747         for (;;)
3748         {
3749                 file.WriteString(_T("<tr>\n"));
3750                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3751                 {
3752                         for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3753                         {
3754                                 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3755                                         break;
3756                         }
3757                                 
3758                         if (idx[nBuffer] < nLineCount[nBuffer])
3759                         {
3760                                 // line number
3761                                 int iVisibleLineNumber = 0;
3762                                 String tdtag = _T("<td class=\"ln\">");
3763                                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3764                                 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3765                                 {
3766                                         iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3767                                 }
3768                                 if (nBuffer == 0 &&
3769                                         (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3770                                         (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3771                                 {
3772                                         ++nDiff;
3773                                         if (iVisibleLineNumber > 0)
3774                                         {
3775                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3776                                                 iVisibleLineNumber = 0;
3777                                         }
3778                                         else
3779                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3780                                 }
3781                                 if (iVisibleLineNumber > 0)
3782                                         tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3783                                 else
3784                                         tdtag += _T("</td>");
3785                                 file.WriteString(tdtag);
3786                                 // line content
3787                                 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3788                                 idx[nBuffer]++;
3789                         }
3790                         else
3791                                 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3792                         file.WriteString(_T("\n"));
3793                 }
3794                 file.WriteString(_T("</tr>\n"));
3795
3796                 bool bBorderLine = false;
3797                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3798                 {
3799                         if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3800                                 bBorderLine = true;
3801                 }
3802
3803                 if (bBorderLine)
3804                 {
3805                         file.WriteString(_T("<tr height=1>"));
3806                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3807                         {
3808                                 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3809                                         file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3810                                 else
3811                                         file.WriteString(_T("<td></td><td></td>"));
3812                         }
3813                         file.WriteString(_T("</tr>\n"));
3814                 }
3815
3816                 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3817                         break;
3818         }
3819         file.WriteString(
3820                 _T("</tbody>\n")
3821                 _T("</table>\n")
3822                 _T("</body>\n")
3823                 _T("</html>\n"));
3824
3825         file.Close();
3826
3827         return true;
3828 }
3829
3830 /**
3831  * @brief Generate report from file compare results.
3832  */
3833 void CMergeDoc::OnToolsGenerateReport()
3834 {
3835         String s;
3836         CString folder;
3837
3838         if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3839                 return;
3840
3841         if (GenerateReport(s.c_str()))
3842                 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3843 }
3844
3845 /**
3846  * @brief Generate patch from files selected.
3847  *
3848  * Creates a patch from selected files in active directory compare, or
3849  * active file compare. Files in file compare must be saved before
3850  * creating a patch.
3851  */
3852 void CMergeDoc::OnToolsGeneratePatch()
3853 {
3854         // If there are changes in files, tell user to save them first
3855         if (IsModified())
3856         {
3857                 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3858                 return;
3859         }
3860
3861         CPatchTool patcher;
3862         patcher.AddFiles(m_filePaths.GetLeft(),
3863                         m_filePaths.GetRight());
3864         patcher.CreatePatch();
3865 }
3866
3867 /**
3868  * @brief Add synchronization point
3869  */
3870 void CMergeDoc::AddSyncPoint()
3871 {
3872         int nLine[3];
3873         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3874         {
3875                 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3876                 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3877         }
3878
3879         // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3880         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3881                 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3882                 {
3883                         LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3884                         return;
3885                 }
3886
3887         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3888                 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3889                         DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3890
3891         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3892                 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3893
3894         m_bHasSyncPoints = true;
3895
3896         ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3897
3898         FlushAndRescan(true);
3899 }
3900
3901 /**
3902  * @brief Delete a synchronization point
3903  */
3904 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3905 {
3906         const auto syncpoints = GetSyncPointList();     
3907         for (auto syncpnt : syncpoints)
3908         {
3909                 if (syncpnt[pane] == nLine)
3910                 {
3911                         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3912                                 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3913                 }
3914         }
3915
3916         if (syncpoints.size() == 1)
3917                 m_bHasSyncPoints = false;
3918
3919         if (bRescan)
3920                 FlushAndRescan(true);
3921         return true;
3922 }
3923
3924 /**
3925  * @brief Clear Synchronization points
3926  */
3927 void CMergeDoc::ClearSyncPoints()
3928 {
3929         if (!m_bHasSyncPoints)
3930                 return;
3931
3932         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3933         {
3934                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3935                 for (int nLine = 0; nLine < nLineCount; ++nLine)
3936                 {
3937                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3938                                 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3939                 }
3940         }
3941         
3942         m_bHasSyncPoints = false;
3943
3944         FlushAndRescan(true);
3945 }
3946
3947 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3948 {
3949         std::vector<std::vector<int> > list;
3950         if (!m_bHasSyncPoints)
3951                 return list;
3952         int idx[3] = {-1, -1, -1};
3953         std::vector<int> points(m_nBuffers);
3954         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3955                 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3956         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3957         {
3958                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3959                 for (int nLine = 0; nLine < nLineCount; ++nLine)
3960                 {
3961                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3962                         {
3963                                 idx[nBuffer]++;
3964                                 if (static_cast<int>(list.size()) <= idx[nBuffer])
3965                                         list.push_back(points);
3966                                 list[idx[nBuffer]][nBuffer] = nLine;
3967                         }
3968                 }
3969         }
3970         return list;
3971 }
3972