OSDN Git Service

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