OSDN Git Service

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