OSDN Git Service

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