OSDN Git Service

MergeDoc.cpp: Remove unused variables
[winmerge-jp/winmerge-jp.git] / Src / MergeDoc.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  MergeDoc.cpp
9  *
10  * @brief Implementation file for CMergeDoc
11  *
12  */
13
14 #include "StdAfx.h"
15 #include "MergeDoc.h"
16 #include <io.h>
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
19 #include "Merge.h"
20 #include "MainFrm.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
26 #include "DirDoc.h"
27 #include "files.h"
28 #include "FileTransform.h"
29 #include "unicoder.h"
30 #include "UniFile.h"
31 #include "OptionsDef.h"
32 #include "DiffFileInfo.h"
33 #include "SaveClosingDlg.h"
34 #include "OpenTableDlg.h"
35 #include "DiffList.h"
36 #include "paths.h"
37 #include "OptionsMgr.h"
38 #include "OptionsDiffOptions.h"
39 #include "MergeLineFlags.h"
40 #include "FileOrFolderSelect.h"
41 #include "LineFiltersList.h"
42 #include "TempFile.h"
43 #include "codepage_detect.h"
44 #include "SelectUnpackerDlg.h"
45 #include "EncodingErrorBar.h"
46 #include "MergeCmdLineInfo.h"
47 #include "TFile.h"
48 #include "Constants.h"
49 #include "Merge7zFormatMergePluginImpl.h"
50 #include "7zCommon.h"
51 #include "PatchTool.h"
52 #include "charsets.h"
53 #include "markdown.h"
54 #include "stringdiffs.h"
55
56 #ifdef _DEBUG
57 #define new DEBUG_NEW
58 #endif
59
60 using std::swap;
61
62 int CMergeDoc::m_nBuffersTemp = 2;
63
64 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
65
66 /////////////////////////////////////////////////////////////////////////////
67 // CMergeDoc
68
69 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
70
71 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
72         //{{AFX_MSG_MAP(CMergeDoc)
73         ON_COMMAND(ID_FILE_SAVE, OnFileSave)
74         ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
75         ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
76         ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
77         ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
78         ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
79         ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
80         ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
81         ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
82         ON_UPDATE_COMMAND_UI(ID_STATUS_PLUGIN, OnUpdatePluginName)
83         ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
84         ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
85         ON_COMMAND(ID_RESCAN, OnFileReload)
86         ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
87         ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_TOGGLE, OnDiffContext)
88         ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_TOGGLE, OnUpdateDiffContext)
89         ON_COMMAND(ID_POPUP_OPEN_WITH_UNPACKER, OnCtxtOpenWithUnpacker)
90         ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
91         ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
92         ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
93         ON_COMMAND(IDOK, OnOK)
94         ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
95         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
96         ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
97         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
98         ON_COMMAND(ID_MERGE_COMPARE_XML, OnFileRecompareAsXML)
99         ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_XML, OnUpdateFileRecompareAsXML)
100         ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
101         //}}AFX_MSG_MAP
102 END_MESSAGE_MAP()
103
104 /////////////////////////////////////////////////////////////////////////////
105 // CMergeDoc construction/destruction
106
107 /**
108  * @brief Constructor.
109  */
110 CMergeDoc::CMergeDoc()
111 : m_bEnableRescan(true)
112 , m_nCurDiff(-1)
113 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
114 , m_pDirDoc(nullptr)
115 , m_bMixedEol(false)
116 , m_pInfoUnpacker(new PackingInfo)
117 , m_pEncodingErrorBar(nullptr)
118 , m_bHasSyncPoints(false)
119 , m_bAutoMerged(false)
120 , m_nGroups(0)
121 , m_pView{nullptr}
122 {
123         DIFFOPTIONS options = {0};
124
125         m_nBuffers = m_nBuffersTemp;
126         m_filePaths.SetSize(m_nBuffers);
127
128         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
129         {
130                 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
131                 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
132                 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
133                 m_nBufferType[nBuffer] = BUFFER_NORMAL;
134                 m_bEditAfterRescan[nBuffer] = false;
135         }
136
137         m_bEnableRescan = true;
138         // COleDateTime m_LastRescan
139         curUndo = undoTgt.begin();
140         m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
141
142         m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
143         Options::DiffOptions::Load(GetOptionsMgr(), options);
144
145         m_diffWrapper.SetOptions(&options);
146         m_diffWrapper.SetPrediffer(nullptr);
147 }
148
149 /**
150  * @brief Destructor.
151  *
152  * Informs associated dirdoc that mergedoc is closing.
153  */
154 CMergeDoc::~CMergeDoc()
155 {       
156         if (m_pDirDoc != nullptr)
157         {
158                 m_pDirDoc->MergeDocClosing(this);
159                 m_pDirDoc = nullptr;
160         }
161 }
162
163 /**
164  * @brief Deleted data associated with doc before closing.
165  */
166 void CMergeDoc::DeleteContents ()
167 {
168         CDocument::DeleteContents ();
169         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
170         {
171                 m_ptBuf[nBuffer]->FreeAll ();
172                 m_tempFiles[nBuffer].Delete();
173         }
174 }
175
176 /**
177  * @brief Called when new document is created.
178  *
179  * Initialises buffers.
180  */
181 BOOL CMergeDoc::OnNewDocument()
182 {
183         if (!CDocument::OnNewDocument())
184                 return false;
185
186         SetTitle(_("File Comparison").c_str());
187         
188         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
189                 m_ptBuf[nBuffer]->InitNew ();
190         return true;
191 }
192
193 /**
194  * @brief Return active merge edit view (or left one if neither active)
195  */
196 CMergeEditView * CMergeDoc::GetActiveMergeView()
197 {
198         CView * pActiveView = GetParentFrame()->GetActiveView();
199         CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
200         if (pMergeEditView == nullptr)
201                 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
202         return pMergeEditView;
203 }
204
205 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
206 {
207         return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
208 }
209
210 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
211 {
212         if (infoNewHandler != nullptr)
213         {
214                 *m_pInfoUnpacker = *infoNewHandler;
215         }
216 }
217
218 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
219 {
220         m_diffWrapper.SetPrediffer(infoPrediffer);
221 }
222 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
223 {
224         m_diffWrapper.GetPrediffer(infoPrediffer);
225 }
226
227 /////////////////////////////////////////////////////////////////////////////
228 // CMergeDoc serialization
229
230 void CMergeDoc::Serialize(CArchive& ar)
231 {
232         ASSERT(false); // we do not use CDocument serialization
233 }
234
235 /////////////////////////////////////////////////////////////////////////////
236 // CMergeDoc commands
237
238 /**
239  * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
240  *
241  * @note 
242  * original file is Ansi : 
243  *   buffer  -> save as Ansi -> Ansi plugins -> diffutils
244  * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
245  *   buffer  -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
246  * (the plugins are optional, not the conversion)
247  * @todo Show SaveToFile() errors?
248  */
249 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
250 {
251         // and we don't repack the file
252         PackingInfo * tempPacker = nullptr;
253
254         // write buffer out to temporary file
255         String sError;
256         int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
257                 CRLF_STYLE_AUTOMATIC, false, nStartLine, nLines);
258 }
259
260 /**
261  * @brief Save files to temp files & compare again.
262  *
263  * @param bBinary [in,out] [in] If true, compare two binary files
264  * [out] If true binary file was detected.
265  * @param bIdentical [out] If true files were identical
266  * @param bForced [in] If true, suppressing is ignored and rescan
267  * is done always
268  * @return Tells if rescan was successfully, was suppressed, or
269  * error happened
270  * If this code is OK, Rescan has detached the views temporarily
271  * (positions of cursors have been lost)
272  * @note Rescan() ALWAYS compares temp files. Actual user files are not
273  * touched by Rescan().
274  * @sa CDiffWrapper::RunFileDiff()
275  */
276 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
277                 bool bForced /* =false */)
278 {
279         DIFFOPTIONS diffOptions = {0};
280         DiffFileInfo fileInfo;
281         bool diffSuccess = false;
282         int nResult = RESCAN_OK;
283         FileChange FileChanged[3] = {FileNoChange, FileNoChange, FileNoChange};
284         int nBuffer;
285
286         if (!bForced)
287         {
288                 if (!m_bEnableRescan)
289                         return RESCAN_SUPPRESSED;
290         }
291
292         ClearWordDiffCache();
293
294         if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
295         {
296                 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
297         }
298         else
299         {
300                 m_diffWrapper.SetFilterList(_T(""));
301         }
302         if (GetView(0, 0)->m_CurSourceDef->type != 0)
303                 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
304         else
305                 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
306
307         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
308         {
309                 // Check if files have been modified since last rescan
310                 // Ignore checking in case of scratchpads (empty filenames)
311                 if (!m_filePaths[nBuffer].empty())
312                 {
313                         FileChanged[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
314                                         fileInfo, false, nBuffer);
315                 }
316         }
317         m_LastRescan = COleDateTime::GetCurrentTime();
318
319         LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
320         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
321         {
322                 if (FileChanged[nBuffer] == FileRemoved)
323                 {
324                         String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
325                         ShowMessageBox(msg, MB_ICONWARNING);
326                         bool bSaveResult = false;
327                         bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
328                         if (!ok || !bSaveResult)
329                         {
330                                 return RESCAN_FILE_ERR;
331                         }
332                 }
333
334                 String temp = m_tempFiles[nBuffer].GetPath();
335                 if (temp.empty())
336                         temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
337                 if (temp.empty())
338                         return RESCAN_TEMP_ERR;
339         }
340
341         CheckFileChanged();
342
343         String tempPath = env::GetTemporaryPath();
344
345         // Set up DiffWrapper
346         m_diffWrapper.GetOptions(&diffOptions);
347
348         // Clear diff list
349         m_diffList.Clear();
350         m_nCurDiff = -1;
351         m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
352         // Clear moved lines lists
353         if (m_diffWrapper.GetDetectMovedBlocks())
354         {
355                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
356                         m_diffWrapper.GetMovedLines(nBuffer)->Clear();
357         }
358
359         // Set paths for diffing and run diff
360         m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
361         if (m_nBuffers < 3)
362                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
363         else
364                 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
365         m_diffWrapper.SetCompareFiles(m_filePaths);
366
367         DIFFSTATUS status;
368
369         if (!HasSyncPoints())
370         {
371                 // Save text buffer to file
372                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
373                 {
374                         m_ptBuf[nBuffer]->SetTempPath(tempPath);
375                         SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
376                 }
377
378                 m_diffWrapper.SetCreateDiffList(&m_diffList);
379                 diffSuccess = m_diffWrapper.RunFileDiff();
380
381                 // Read diff-status
382                 m_diffWrapper.GetDiffStatus(&status);
383                 if (bBinary) // believe caller if we were told these are binaries
384                         status.bBinaries = true;
385         }
386         else
387         {
388                 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();   
389                 int nStartLine[3] = {0};
390                 int nLines[3], nRealLine[3];
391                 for (size_t i = 0; i <= syncpoints.size(); ++i)
392                 {
393                         // Save text buffer to file
394                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
395                         {
396                                 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
397                                 m_ptBuf[nBuffer]->SetTempPath(tempPath);
398                                 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(), 
399                                         nStartLine[nBuffer], nLines[nBuffer]);
400                         }
401                         DiffList templist;
402                         templist.Clear();
403                         m_diffWrapper.SetCreateDiffList(&templist);
404                         diffSuccess = m_diffWrapper.RunFileDiff();
405                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
406                                 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
407
408                         // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
409                         if (i == 0 && templist.GetSize() > 0)
410                                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
411                                         if (nStartLine[nBuffer] == 0)
412                                         {
413                                                 bool isEmptyFile = true;
414                                                 for (int j = 0; j < nLines[nBuffer]; j++)
415                                                 {
416                                                         if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
417                                                         {
418                                                                 isEmptyFile = false;
419                                                                 break;
420                                                         }
421                                                 }
422                                                 if (isEmptyFile)
423                                                 {
424                                                         DIFFRANGE di;
425                                                         templist.GetDiff(0, di);
426                                                         if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
427                                                         {
428                                                                 di.end[nBuffer] = -1;
429                                                                 templist.SetDiff(0, di);
430                                                         }
431                                                 }
432                                         }
433
434                         m_diffList.AppendDiffList(templist, nRealLine);
435                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
436                                 nStartLine[nBuffer] += nLines[nBuffer];
437
438                         // Read diff-status
439                         DIFFSTATUS status_part;
440                         m_diffWrapper.GetDiffStatus(&status_part);
441                         if (bBinary) // believe caller if we were told these are binaries
442                                 status.bBinaries = true;
443                         status.MergeStatus(status_part);
444                 }
445                 m_diffWrapper.SetCreateDiffList(&m_diffList);
446         }
447
448         // If one file has EOL before EOF and other not...
449         if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
450         {
451                 // ..last DIFFRANGE of file which has EOL must be
452                 // fixed to contain last line too
453                 int lineCount[3] = { 0,0,0 };
454                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
455                         lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
456                 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
457         }
458
459         // set identical/diff result as recorded by diffutils
460         identical = status.Identical;
461
462         // Determine errors and binary file compares
463         if (!diffSuccess)
464                 nResult = RESCAN_FILE_ERR;
465         else if (status.bBinaries)
466         {
467                 bBinary = true;
468         }
469         else
470         {
471                 // Now update views and buffers for ghost lines
472
473                 // Prevent displaying views during this update 
474                 // BTW, this solves the problem of double asserts
475                 // (during the display of an assert message box, a second assert in one of the 
476                 //  display functions happens, and hides the first assert)
477                 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
478
479                 // Remove blank lines and clear winmerge flags
480                 // this operation does not change the modified flag
481                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
482                         m_ptBuf[nBuffer]->prepareForRescan();
483
484                 // Divide diff blocks to match lines.
485                 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
486                         AdjustDiffBlocks();
487
488                 // Analyse diff-list (updating real line-numbers)
489                 // this operation does not change the modified flag
490                 PrimeTextBuffers();
491
492                 // Hide identical lines if diff-context is not 'All'
493                 HideLines();
494
495                 // Apply flags to lines that are trivial
496                 PrediffingInfo infoPrediffer;
497                 GetPrediffer(&infoPrediffer);
498                 if (!infoPrediffer.m_PluginName.empty())
499                         FlagTrivialLines();
500                 
501                 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
502                 if (m_diffWrapper.GetDetectMovedBlocks())
503                         FlagMovedLines();
504                 
505                 // After PrimeTextBuffers() we know amount of real diffs
506                 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
507
508                 // Identical files are also updated
509                 if (!m_diffList.HasSignificantDiffs())
510                         identical = IDENTLEVEL_ALL;
511
512                 ForEachView([](auto& pView) {
513                         // just apply some options to the views
514                         pView->PrimeListWithFile();
515                         // Now buffers data are valid
516                         pView->ReAttachToBuffer();
517                 });
518                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
519                 {
520                         m_bEditAfterRescan[nBuffer] = false;
521                 }
522         }
523
524         if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
525                 identical == IDENTLEVEL_ALL &&
526                 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
527                         [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
528                 identical = IDENTLEVEL_NONE;
529
530         GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL_ALL ? 1 : 0);
531
532         return nResult;
533 }
534
535 void CMergeDoc::CheckFileChanged(void)
536 {
537         int nBuffer;
538         DiffFileInfo fileInfo;
539         FileChange FileChange[3];
540
541         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
542         {
543                 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
544                         false, nBuffer);
545
546                 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
547         }
548
549         bool bDoReload = false;
550         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
551         {
552                 if (FileChange[nBuffer] == FileChanged)
553                 {
554                         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]);
555                         if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
556                                 bDoReload = true;
557                         break;
558                 }
559         }
560         if (bDoReload)
561         {
562                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
563                 {
564                         if (FileChange[nBuffer] == FileChanged)
565                         {
566                                 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
567                                 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
568                         }
569                 }
570         }
571 }
572
573 /** @brief Apply flags to lines that are trivial */
574 void CMergeDoc::FlagTrivialLines(void)
575 {
576         for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
577         {
578                 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
579                 {
580                         String str[3];
581                         for (int file = 0; file < m_nBuffers; ++file)
582                         {
583                                 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
584                                 str[file] = p ? p : _T("");
585                         }
586
587                         if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
588                         {
589                                 DIFFOPTIONS diffOptions = {0};
590                                 m_diffWrapper.GetOptions(&diffOptions);
591
592                                 // Make the call to stringdiffs, which does all the hard & tedious computations
593                                 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
594                                         !diffOptions.bIgnoreCase,
595                                         !diffOptions.bIgnoreEol,
596                                         diffOptions.nIgnoreWhitespace,
597                                         GetBreakType(), // whitespace only or include punctuation
598                                         GetByteColoringOption());
599                                 if (!worddiffs.empty())
600                                 {
601                                         for (int file = 0; file < m_nBuffers; ++file)
602                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
603                                 }
604                         }
605                 }
606         }
607 }
608
609 /** @brief Adjust all different lines that were detected as actually matching moved lines */
610 void CMergeDoc::FlagMovedLines(void)
611 {
612         int i;
613         MovedLines *pMovedLines;
614
615         pMovedLines = m_diffWrapper.GetMovedLines(0);
616         for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
617         {
618                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_RIGHT);
619                 if (j != -1)
620                 {
621                         TRACE(_T("%d->%d\n"), i, j);
622                         ASSERT(j>=0);
623                         // We only flag lines that are already marked as being different
624                         int apparent = m_ptBuf[0]->ComputeApparentLine(i);
625                         if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
626                         {
627                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
628                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
629                                 {
630                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
631                                         if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
632                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
633                                 }
634                         }
635                 }
636         }
637
638         pMovedLines = m_diffWrapper.GetMovedLines(1);
639         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
640         {
641                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_LEFT);
642                 if (j != -1)
643                 {
644                         TRACE(_T("%d->%d\n"), i, j);
645                         ASSERT(j>=0);
646                         // We only flag lines that are already marked as being different
647                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
648                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
649                         {
650                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
651                                 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
652                                 {
653                                         int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
654                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
655                                                 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
656                                 }
657                         }
658                 }
659         }
660
661         if (m_nBuffers < 3)
662                 return;
663
664         pMovedLines = m_diffWrapper.GetMovedLines(1);
665         for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
666         {
667                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_RIGHT);
668                 if (j != -1)
669                 {
670                         TRACE(_T("%d->%d\n"), i, j);
671                         ASSERT(j>=0);
672                         // We only flag lines that are already marked as being different
673                         int apparent = m_ptBuf[1]->ComputeApparentLine(i);
674                         if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
675                         {
676                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
677                                 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
678                                 {
679                                         int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
680                                         if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
681                                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
682                                 }
683                         }
684                 }
685         }
686
687         pMovedLines = m_diffWrapper.GetMovedLines(2);
688         for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
689         {
690                 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE_LEFT);
691                 if (j != -1)
692                 {
693                         TRACE(_T("%d->%d\n"), i, j);
694                         ASSERT(j>=0);
695                         // We only flag lines that are already marked as being different
696                         int apparent = m_ptBuf[2]->ComputeApparentLine(i);
697                         if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
698                         {
699                                 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
700                                 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
701                                 {
702                                         int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
703                                         if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
704                                                 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
705                                 }
706                         }
707                 }
708         }
709
710         // todo: Need to record actual moved information
711 }
712
713 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
714 {
715         if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
716         {
717                 GetParentFrame()->InitialUpdateFrame(this, true);
718                 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
719         }
720         return AfxMessageBox(sText.c_str(), nType, nIDHelp);
721 }
722
723 /**
724  * @brief Prints (error) message by rescan status.
725  *
726  * @param nRescanResult [in] Resultcocode from rescan().
727  * @param bIdentical [in] Were files identical?.
728  * @sa CMergeDoc::Rescan()
729  */
730 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
731 {
732         // Rescan was suppressed, there is no sensible status
733         if (nRescanResult == RESCAN_SUPPRESSED)
734                 return;
735
736         String s;
737
738         if (nRescanResult == RESCAN_FILE_ERR)
739         {
740                 s = _("An error occurred while comparing the files.");
741                 LogErrorString(s);
742                 ShowMessageBox(s, MB_ICONSTOP);
743                 return;
744         }
745
746         if (nRescanResult == RESCAN_TEMP_ERR)
747         {
748                 s = _("Temporary files could not be created. Check your temporary path settings.");
749                 LogErrorString(s);
750                 ShowMessageBox(s, MB_ICONSTOP);
751                 return;
752         }
753
754         // Files are not binaries, but they are identical
755         if (identical != IDENTLEVEL_NONE)
756         {
757                 if (theApp.m_bExitIfNoDiff != MergeCmdLineInfo::ExitQuiet)
758                 {
759                         UINT nFlags = MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN;
760
761                         if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit)
762                         {
763                                 // Show the "files are identical" for basic "exit no diff" flag
764                                 // If user don't want to see the message one uses the quiet version
765                                 // of the "exit no diff".
766                                 nFlags &= ~MB_DONT_DISPLAY_AGAIN;
767                         }
768                         if ((m_nBuffers == 2 && !m_filePaths.GetLeft().empty() && !m_filePaths.GetRight().empty() &&
769                                  strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetRight()) == 0) ||
770                                 (m_nBuffers == 3 && !m_filePaths.GetLeft().empty() && !m_filePaths.GetMiddle().empty() && !m_filePaths.GetRight().empty() &&
771                                  (strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetRight()) == 0 ||
772                                   strutils::compare_nocase(m_filePaths.GetMiddle(), m_filePaths.GetRight()) == 0 ||
773                                   strutils::compare_nocase(m_filePaths.GetLeft(), m_filePaths.GetMiddle()) == 0)))
774                         {
775                                 // compare file to itself, a custom message so user may hide the message in this case only
776                                 s = _("The same file is opened in both panels.");
777                                 ShowMessageBox(s, nFlags, IDS_FILE_TO_ITSELF);
778                         }
779                         else if (identical == IDENTLEVEL_ALL)
780                         {
781                                 s = _("The selected files are identical.");
782                                 ShowMessageBox(s, nFlags, IDS_FILESSAME);
783                         }
784                 }
785
786                 if (identical == IDENTLEVEL_ALL)
787                 {
788                         // Exit application if files are identical.
789                         if (theApp.m_bExitIfNoDiff == MergeCmdLineInfo::Exit ||
790                                 theApp.m_bExitIfNoDiff == MergeCmdLineInfo::ExitQuiet)
791                         {
792                                 AfxGetMainWnd()->PostMessage(WM_COMMAND, ID_APP_EXIT);
793                         }
794                 }
795         }
796 }
797
798 bool CMergeDoc::Undo()
799 {
800         return false;
801 }
802
803 /**
804  * @brief An instance of RescanSuppress prevents rescan during its lifetime
805  * (or until its Clear method is called, which ends its effect).
806  */
807 class RescanSuppress
808 {
809 public:
810         explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
811         {
812                 m_bSuppress = true;
813                 m_bPrev = doc.m_bEnableRescan;
814                 m_doc.m_bEnableRescan = false;
815         }
816         void Clear() 
817         {
818                 if (m_bSuppress)
819                 {
820                         m_bSuppress = false;
821                         m_doc.m_bEnableRescan = m_bPrev;
822                 }
823         }
824         ~RescanSuppress()
825         {
826                 Clear();
827         }
828 private:
829         CMergeDoc & m_doc;
830         bool m_bPrev;
831         bool m_bSuppress;
832 };
833
834 /**
835  * @brief Copy all diffs from one side to side.
836  * @param [in] srcPane Source side from which diff is copied
837  * @param [in] dstPane Destination side
838  */
839 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
840 {
841         CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
842 }
843
844 /**
845  * @brief Copy range of diffs from one side to side.
846  * This function copies given range of differences from side to another.
847  * Ignored differences are skipped, and not copied.
848  * @param [in] srcPane Source side from which diff is copied
849  * @param [in] dstPane Destination side
850  * @param [in] firstDiff First diff copied (0-based index)
851  * @param [in] lastDiff Last diff copied (0-based index)
852  */
853 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
854 {
855 #ifdef _DEBUG
856         if (firstDiff > lastDiff)
857                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
858         if (firstDiff < 0)
859                 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
860         if (lastDiff > m_diffList.GetSize() - 1)
861                 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
862 #endif
863
864         lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
865         firstDiff = max(0, firstDiff);
866         if (firstDiff > lastDiff)
867                 return;
868         
869         RescanSuppress suppressRescan(*this);
870
871         // Note we don't care about m_nDiffs count to become zero,
872         // because we don't rescan() so it does not change
873
874         SetCurrentDiff(lastDiff);
875
876         bool bGroupWithPrevious = false;
877         if (firstWordDiff <= 0 && lastWordDiff == -1)
878         {
879                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
880                         return; // sync failure
881         }
882         else
883         {
884                 if (!WordListCopy(srcPane, dstPane, lastDiff, 
885                         (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
886                         return; // sync failure
887         }
888
889         SetEditedAfterRescan(dstPane);
890
891         int nGroup = GetActiveMergeView()->m_nThisGroup;
892         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
893         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
894         CPoint currentPosSrc = pViewSrc->GetCursorPos();
895         currentPosSrc.x = 0;
896         CPoint currentPosDst = pViewDst->GetCursorPos();
897         currentPosDst.x = 0;
898
899         CPoint pt(0, 0);
900         pViewDst->SetCursorPos(pt);
901         pViewDst->SetNewSelection(pt, pt, false);
902         pViewDst->SetNewAnchor(pt);
903
904         // copy from bottom up is more efficient
905         for (int i = lastDiff - 1; i >= firstDiff; --i)
906         {
907                 if (m_diffList.IsDiffSignificant(i))
908                 {
909                         SetCurrentDiff(i);
910                         const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
911                         if (currentPosDst.y > pdi->dend)
912                         {
913                                 if (pdi->blank[dstPane] >= 0)
914                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
915                                 else if (pdi->blank[srcPane] >= 0)
916                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
917                         }                       
918                         // Group merge with previous (merge undo data to one action)
919                         bGroupWithPrevious = true;
920                         if (i > firstDiff || firstWordDiff <= 0)
921                         {
922                                 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
923                                         break; // sync failure
924                         }
925                         else
926                         {
927                                 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
928                                         break; // sync failure
929                         }
930                 }
931         }
932
933         ForEachView(dstPane, [currentPosDst](auto& pView) {
934                 pView->SetCursorPos(currentPosDst);
935                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
936                 pView->SetNewAnchor(currentPosDst);
937         });
938
939         suppressRescan.Clear(); // done suppress Rescan
940         FlushAndRescan();
941 }
942
943 enum MergeResult { NoMergeNeeded, Merged, Conflict };
944
945 template<class Type>
946 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
947 {
948         bool equal_all = middle == left && middle == right && left == right;
949         if (equal_all)
950                 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
951         bool conflict = middle != left && middle != right && left != right;
952         if (conflict)
953                 return std::pair<MergeResult, Type>(Conflict, left);
954         switch (dstPane)
955         {
956         case 0:
957                 if (left == right)
958                         return std::pair<MergeResult, Type>(Merged, middle);
959                 break;
960         case 1:
961                 if (left == middle)
962                         return std::pair<MergeResult, Type>(Merged, right);
963                 else
964                         return std::pair<MergeResult, Type>(Merged, left);
965                 break;
966         case 2:
967                 if (left == right)
968                         return std::pair<MergeResult, Type>(Merged, middle);
969                 break;
970         }
971         return std::pair<MergeResult, Type>(NoMergeNeeded, left);
972 }
973
974 /**
975  * @brief Do auto-merge.
976  * @param [in] dstPane Destination side
977  */
978 void CMergeDoc::DoAutoMerge(int dstPane)
979 {
980         const int lastDiff = m_diffList.GetSize() - 1;
981         const int firstDiff = 0;
982         bool bGroupWithPrevious = false;
983         int autoMergedCount = 0;
984         int unresolvedConflictCount = 0;
985
986         std::pair<MergeResult, FileTextEncoding> mergedEncoding = 
987                 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
988         if (mergedEncoding.first == Merged)
989         {
990                 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
991                 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
992         }
993         else if (mergedEncoding.first == Conflict)
994                 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
995
996         std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle = 
997                 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
998         if (mergedEOLStyle.first == Merged)
999         {
1000                 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1001                 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1002         }
1003         else if (mergedEOLStyle.first == Conflict)
1004                 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1005
1006         RescanSuppress suppressRescan(*this);
1007
1008         // Note we don't care about m_nDiffs count to become zero,
1009         // because we don't rescan() so it does not change
1010
1011         SetCurrentDiff(lastDiff);
1012
1013         SetEditedAfterRescan(dstPane);
1014
1015         int nGroup = GetActiveMergeView()->m_nThisGroup;
1016         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1017         CPoint currentPosDst = pViewDst->GetCursorPos();
1018         currentPosDst.x = 0;
1019
1020         CPoint pt(0, 0);
1021         pViewDst->SetCursorPos(pt);
1022         pViewDst->SetNewSelection(pt, pt, false);
1023         pViewDst->SetNewAnchor(pt);
1024
1025         // copy from bottom up is more efficient
1026         for (int i = lastDiff; i >= firstDiff; --i)
1027         {
1028                 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1029                 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1030                 if (srcPane != -1)
1031                 {
1032                         SetCurrentDiff(i);
1033                         if (currentPosDst.y > pdi->dend)
1034                         {
1035                                 if (pdi->blank[dstPane] >= 0)
1036                                         currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1037                                 else if (pdi->blank[srcPane] >= 0)
1038                                         currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1039                         }                       
1040                         // Group merge with previous (merge undo data to one action)
1041                         if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1042                                 break; // sync failure
1043                         if (!bGroupWithPrevious)
1044                                 bGroupWithPrevious = true;
1045                         ++autoMergedCount;
1046                 }
1047                 if (pdi->op == OP_DIFF)
1048                         ++unresolvedConflictCount;
1049         }
1050
1051         ForEachView(dstPane, [currentPosDst](auto& pView) {
1052                 pView->SetCursorPos(currentPosDst);
1053                 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1054                 pView->SetNewAnchor(currentPosDst);
1055         });
1056
1057         suppressRescan.Clear(); // done suppress Rescan
1058         FlushAndRescan();
1059         UpdateHeaderPath(dstPane);
1060
1061         if (autoMergedCount > 0)
1062                 m_bAutoMerged = true;
1063
1064         // move to first conflict 
1065         const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1066         if (nDiff != -1)
1067                 pViewDst->SelectDiff(nDiff, true, false);
1068
1069         ShowMessageBox(
1070                 strutils::format_string2(
1071                         _T("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"), 
1072                         strutils::format(_T("%d"), autoMergedCount),
1073                         strutils::format(_T("%d"), unresolvedConflictCount)),
1074                 MB_ICONINFORMATION);
1075 }
1076
1077 /**
1078  * @brief Sanity check difference.
1079  *
1080  * Checks that lines in difference are inside difference in both files.
1081  * If file is edited, lines added or removed diff lines get out of sync and
1082  * merging fails miserably.
1083  *
1084  * @param [in] dr Difference to check.
1085  * @return true if difference lines match, false otherwise.
1086  */
1087 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1088 {
1089         const int cd_dbegin = dr.dbegin;
1090         const int cd_dend = dr.dend;
1091
1092         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1093         {
1094                 // Must ensure line number is in range before getting line flags
1095                 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1096                         return false;
1097
1098                 // Optimization - check last line first so we don't need to
1099                 // check whole diff for obvious cases
1100                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1101                 if (!(dwFlags & LF_WINMERGE_FLAGS))
1102                         return false;
1103         }
1104
1105         for (int line = cd_dbegin; line < cd_dend; line++)
1106         {
1107                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1108                 {
1109                         DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1110                         if (!(dwFlags & LF_WINMERGE_FLAGS))
1111                                 return false;
1112                 }
1113         }
1114         return true;
1115 }
1116
1117 /**
1118  * @brief Copy selected (=current) difference from from side to side.
1119  * @param [in] srcPane Source side from which diff is copied
1120  * @param [in] dstPane Destination side
1121  * @param [in] nDiff Diff to copy, if -1 function determines it.
1122  * @param [in] bGroupWithPrevious Adds diff to same undo group with
1123  * @return true if ok, false if sync failure & need to abort copy
1124  * previous action (allows one undo for copy all)
1125  */
1126 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1127                 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1128 {
1129         int nGroup = GetActiveMergeView()->m_nThisGroup;
1130         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1131         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1132         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1133
1134         // suppress Rescan during this method
1135         // (Not only do we not want to rescan a lot of times, but
1136         // it will wreck the line status array to rescan as we merge)
1137         RescanSuppress suppressRescan(*this);
1138
1139         // If diff-number not given, determine it from active view
1140         if (nDiff == -1)
1141         {
1142                 nDiff = GetCurrentDiff();
1143
1144                 // No current diff, but maybe cursor is in diff?
1145                 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1146                         pViewDst->IsCursorInDiff()))
1147                 {
1148                         // Find out diff under cursor
1149                         CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1150                         nDiff = m_diffList.LineToDiff(ptCursor.y);
1151                 }
1152         }
1153
1154         if (nDiff != -1)
1155         {
1156                 DIFFRANGE cd;
1157                 VERIFY(m_diffList.GetDiff(nDiff, cd));
1158                 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1159                 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1160                 bool bSrcWasMod = sbuf.IsModified();
1161                 const int cd_dbegin = cd.dbegin;
1162                 const int cd_dend = cd.dend;
1163                 const int cd_blank = cd.blank[srcPane];
1164                 bool bInSync = SanityCheckDiff(cd);
1165
1166                 if (!bInSync)
1167                 {
1168                         LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1169                         return false; // abort copying
1170                 }
1171
1172                 // If we remove whole diff from current view, we must fix cursor
1173                 // position first. Normally we would move to end of previous line,
1174                 // but we want to move to begin of that line for usability.
1175                 if (bUpdateView)
1176                 {
1177                         CPoint currentPos = pViewDst->GetCursorPos();
1178                         currentPos.x = 0;
1179                         if (currentPos.y > cd_dend)
1180                         {
1181                                 if (cd.blank[dstPane] >= 0)
1182                                         currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1183                                 else if (cd.blank[srcPane] >= 0)
1184                                         currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1185                         }
1186                         ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1187                 }
1188
1189                 // if the current diff contains missing lines, remove them from both sides
1190                 int limit = cd_dend;
1191
1192                 // curView is the view which is changed, so the opposite of the source view
1193                 dbuf.BeginUndoGroup(bGroupWithPrevious);
1194                 if (cd_blank>=0)
1195                 {
1196                         // text was missing, so delete rest of lines on both sides
1197                         // delete only on destination side since rescan will clear the other side
1198                         if (cd_dend + 1 < dbuf.GetLineCount())
1199                         {
1200                                 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1201                         }
1202                         else
1203                         {
1204                                 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1205                                 ASSERT(cd_blank > 0);
1206                                 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1207                         }
1208
1209                         limit=cd_blank-1;
1210                         dbuf.FlushUndoGroup(pSource);
1211                         dbuf.BeginUndoGroup(true);
1212                 }
1213
1214
1215                 // copy the selected text over
1216                 if (cd_dbegin <= limit)
1217                 {
1218                         // text exists on left side, so just replace
1219                         dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1220                         dbuf.FlushUndoGroup(pSource);
1221                         dbuf.BeginUndoGroup(true);
1222                 }
1223                 dbuf.FlushUndoGroup(pSource);
1224
1225                 // remove the diff
1226                 SetCurrentDiff(-1);
1227
1228                 // reset the mod status of the source view because we do make some
1229                 // changes, but none that concern the source text
1230                 sbuf.SetModified(bSrcWasMod);
1231         }
1232
1233         suppressRescan.Clear(); // done suppress Rescan
1234         FlushAndRescan();
1235         return true;
1236 }
1237
1238 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1239                 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1240 {
1241         int nGroup = GetActiveMergeView()->m_nThisGroup;
1242         CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1243         CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1244         CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1245
1246         // suppress Rescan during this method
1247         // (Not only do we not want to rescan a lot of times, but
1248         // it will wreck the line status array to rescan as we merge)
1249         RescanSuppress suppressRescan(*this);
1250
1251         DIFFRANGE cd;
1252         VERIFY(m_diffList.GetDiff(nDiff, cd));
1253         CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1254         CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1255         bool bSrcWasMod = sbuf.IsModified();
1256         const int cd_dbegin = cd.dbegin;
1257         const int cd_dend = cd.dend;
1258         const int cd_blank = cd.blank[srcPane];
1259         bool bInSync = SanityCheckDiff(cd);
1260
1261         if (!bInSync)
1262         {
1263                 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1264                 return false; // abort copying
1265         }
1266
1267         std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1268
1269         if (worddiffs.empty())
1270                 return false;
1271
1272         if (cd.end[srcPane] < cd.begin[srcPane])
1273                 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1274
1275         if (firstWordDiff == -1)
1276                 firstWordDiff = 0;
1277         if (lastWordDiff == -1)
1278                 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1279
1280         // If we remove whole diff from current view, we must fix cursor
1281         // position first. Normally we would move to end of previous line,
1282         // but we want to move to begin of that line for usability.
1283         if (bUpdateView)
1284         {
1285                 CPoint currentPos = pViewDst->GetCursorPos();
1286                 currentPos.x = 0;
1287                 if (currentPos.y > cd_dend)
1288                 {
1289                         if (cd.blank[dstPane] >= 0)
1290                                 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1291                         else if (cd.blank[srcPane] >= 0)
1292                                 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1293                 }
1294                 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1295         }
1296
1297         // if the current diff contains missing lines, remove them from both sides
1298         int limit = cd_dend;
1299
1300         // curView is the view which is changed, so the opposite of the source view
1301         dbuf.BeginUndoGroup(bGroupWithPrevious);
1302
1303         CString srcText, dstText;
1304         CPoint ptDstStart, ptDstEnd;
1305         CPoint ptSrcStart, ptSrcEnd;
1306
1307         ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1308         ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1309         ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1310         ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1311         ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1312         ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1313         ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1314         ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1315
1316         std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1317         std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1318
1319         dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1320         sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1321
1322         nDstOffsets[0] = 0;
1323         for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1324                 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1325         nSrcOffsets[0] = 0;
1326         for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1327                 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1328
1329         for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1330         {
1331                 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1332                         continue;
1333                 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1334                 int srcEnd   = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1335                 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1336                 int dstEnd   = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1337                 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1338                         + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1339                         + dstText.Mid(dstEnd - ptDstStart.x);
1340         }
1341
1342         dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1343
1344         int endl,endc;
1345         dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1346
1347         dbuf.FlushUndoGroup(pSource);
1348
1349         // reset the mod status of the source view because we do make some
1350         // changes, but none that concern the source text
1351         sbuf.SetModified(bSrcWasMod);
1352
1353         suppressRescan.Clear(); // done suppress Rescan
1354         FlushAndRescan();
1355
1356         return true;
1357 }
1358
1359 /**
1360  * @brief Save file with new filename.
1361  *
1362  * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1363  * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1364  * normal filename fails, to let user choose another filename/location.
1365  * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1366  * using this function.
1367  * @param [in, out] strPath 
1368  * - [in] : Initial path shown to user
1369  * - [out] : Path to new filename if saving succeeds
1370  * @param [in, out] nSaveResult 
1371  * - [in] : Statuscode telling why we ended up here. Maybe the result of
1372  * previous save.
1373  * - [out] : Statuscode of this saving try
1374  * @param [in, out] sError Error string from lower level saving code
1375  * @param [in] nBuffer Buffer we are saving
1376  * @return false as long as the user is not satisfied. Calling function
1377  * should not continue until true is returned.
1378  * @sa CMergeDoc::DoSave()
1379  * @sa CMergeDoc::DoSaveAs()
1380  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1381  */
1382 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1383         int nBuffer, PackingInfo * pInfoTempUnpacker)
1384 {
1385         String s;
1386         String str;
1387         String strSavePath; // New path for next saving try
1388         String title;
1389         bool result = true;
1390         int answer = IDOK; // Set default we use for scratchpads
1391
1392         // We shouldn't get here if saving is succeed before
1393         ASSERT(nSaveResult != SAVE_DONE);
1394
1395         // Select message based on reason function called
1396         if (nSaveResult == SAVE_PACK_FAILED)
1397         {
1398                 if (m_nBuffers == 3)
1399                 {
1400                         str = strutils::format_string2(
1401                                 nBuffer == 0 ? 
1402                                         _("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?")
1403                                         : (nBuffer == 1 ? 
1404                                         _("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?"): 
1405                                         _("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?")),
1406                                 strPath, pInfoTempUnpacker->m_PluginName);
1407                 }
1408                 else
1409                 {
1410                         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?") : 
1411                                 _("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?"),
1412                                 strPath, pInfoTempUnpacker->m_PluginName);
1413                 }
1414                 // replace the unpacker with a "do nothing" unpacker
1415                 pInfoTempUnpacker->Initialize(PLUGIN_MANUAL);
1416         }
1417         else
1418         {
1419                 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);
1420         }
1421
1422         // SAVE_NO_FILENAME is temporarily used for scratchpad.
1423         // So don't ask about saving in that case.
1424         if (nSaveResult != SAVE_NO_FILENAME)
1425                 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1426
1427         switch (answer)
1428         {
1429         case IDOK:
1430                 if (nBuffer == 0)
1431                         title = _("Save Left File As");
1432                 else if (nBuffer == m_nBuffers - 1)
1433                         title = _("Save Right File As");
1434                 else
1435                         title = _("Save Middle File As");
1436
1437                 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1438                 {
1439                         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1440                         strSavePath = s;
1441                         nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1442                                 pInfoTempUnpacker);
1443
1444                         if (nSaveResult == SAVE_DONE)
1445                         {
1446                                 // We are saving scratchpad (unnamed file)
1447                                 if (strPath.empty())
1448                                 {
1449                                         m_nBufferType[nBuffer] = BUFFER_UNNAMED_SAVED;
1450                                         m_strDesc[nBuffer].erase();
1451                                 }
1452                                         
1453                                 strPath = strSavePath;
1454                                 UpdateHeaderPath(nBuffer);
1455                         }
1456                         else
1457                                 result = false;
1458                 }
1459                 else
1460                         nSaveResult = SAVE_CANCELLED;
1461                 break;
1462
1463         case IDCANCEL:
1464                 nSaveResult = SAVE_CANCELLED;
1465                 break;
1466         }
1467         return result;
1468 }
1469
1470 /**
1471  * @brief Save file creating backups etc.
1472  *
1473  * Safe top-level file saving function. Checks validity of given path.
1474  * Creates backup file if wanted to. And if saving to given path fails,
1475  * allows user to select new location/name for file.
1476  * @param [in] szPath Path where to save including filename. Can be
1477  * empty/`nullptr` if new file is created (scratchpad) without filename.
1478  * @param [out] bSaveSuccess Will contain information about save success with
1479  * the original name (to determine if file statuses should be changed)
1480  * @param [in] nBuffer Index (0-based) of buffer to save
1481  * @return Tells if caller can continue (no errors happened)
1482  * @note Return value does not tell if SAVING succeeded. Caller must
1483  * Check value of bSaveSuccess parameter after calling this function!
1484  * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1485  * to directory it points to. If m_strSaveAsPath contains filename,
1486  * that filename is used.
1487  * @sa CMergeDoc::TrySaveAs()
1488  * @sa CMainFrame::CheckSavePath()
1489  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1490  */
1491 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1492 {
1493         DiffFileInfo fileInfo;
1494         String strSavePath(szPath);
1495         FileChange fileChanged;
1496         bool bApplyToAll = false;       
1497         int nRetVal = -1;
1498
1499         fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1500         if (fileChanged == FileChanged)
1501         {
1502                 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1503                 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1504                 {
1505                         bSaveSuccess = true;
1506                         return true;
1507                 }               
1508         }
1509
1510         // use a temp packer
1511         // first copy the m_pInfoUnpacker
1512         // if an error arises during packing, change and take a "do nothing" packer
1513         PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1514
1515         bSaveSuccess = false;
1516         
1517         // Check third arg possibly given from command-line
1518         if (!theApp.m_strSaveAsPath.empty())
1519         {
1520                 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1521                 {
1522                         // third arg was a directory, so get append the filename
1523                         String sname;
1524                         paths::SplitFilename(szPath, 0, &sname, 0);
1525                         strSavePath = theApp.m_strSaveAsPath;
1526                         strSavePath = paths::ConcatPath(strSavePath, sname);
1527                 }
1528                 else
1529                         strSavePath = theApp.m_strSaveAsPath;   
1530         }
1531
1532         nRetVal = theApp.HandleReadonlySave(strSavePath, false, bApplyToAll);
1533         if (nRetVal == IDCANCEL)
1534                 return false;
1535
1536         if (!theApp.CreateBackup(false, strSavePath))
1537                 return false;
1538
1539         // false as long as the user is not satisfied
1540         // true if saving succeeds, even with another filename, or if the user cancels
1541         bool result = false;
1542         // the error code from the latest save operation, 
1543         // or SAVE_DONE when the save succeeds
1544         // TODO: Shall we return this code in addition to bSaveSuccess ?
1545         int nSaveErrorCode = SAVE_DONE;
1546         CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1547
1548         // Assume empty filename means Scratchpad (unnamed file)
1549         // Todo: This is not needed? - buffer type check should be enough
1550         if (strSavePath.empty())
1551                 nSaveErrorCode = SAVE_NO_FILENAME;
1552
1553         // Handle unnamed buffers
1554         if (m_nBufferType[nBuffer] == BUFFER_UNNAMED)
1555                 nSaveErrorCode = SAVE_NO_FILENAME;
1556
1557         String sError;
1558         if (nSaveErrorCode == SAVE_DONE)
1559                 // We have a filename, just try to save
1560                 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, &infoTempUnpacker);
1561
1562         if (nSaveErrorCode != SAVE_DONE)
1563         {
1564                 // Saving failed, user may save to another location if wants to
1565                 do
1566                         result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1567                 while (!result);
1568         }
1569
1570         // Saving succeeded with given/selected filename
1571         if (nSaveErrorCode == SAVE_DONE)
1572         {
1573                 // Preserve file times if user wants to
1574                 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1575                 {
1576                         fileInfo.SetFile(strSavePath);
1577                         try
1578                         {
1579                                 TFile file(strSavePath);
1580                                 file.setLastModified(fileInfo.mtime);
1581                         }
1582                         catch (...)
1583                         {
1584                         }
1585                 }
1586
1587                 m_ptBuf[nBuffer]->SetModified(false);
1588                 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1589                 m_filePaths[nBuffer] = strSavePath;
1590                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1591                 UpdateHeaderPath(nBuffer);
1592                 bSaveSuccess = true;
1593                 result = true;
1594         }
1595         else if (nSaveErrorCode == SAVE_CANCELLED)
1596         {
1597                 // User cancelled current operation, lets do what user wanted to do
1598                 result = false;
1599         }
1600         return result;
1601 }
1602
1603 /**
1604  * @brief Save file with different filename.
1605  *
1606  * Safe top-level file saving function. Asks user to select filename
1607  * and path. Does not create backups.
1608  * @param [in] szPath Path where to save including filename. Can be
1609  * empty/`nullptr` if new file is created (scratchpad) without filename.
1610  * @param [out] bSaveSuccess Will contain information about save success with
1611  * the original name (to determine if file statuses should be changed)
1612  * @param [in] nBuffer Index (0-based) of buffer to save
1613  * @return Tells if caller can continue (no errors happened)
1614  * @note Return value does not tell if SAVING succeeded. Caller must
1615  * Check value of bSaveSuccess parameter after calling this function!
1616  * @sa CMergeDoc::TrySaveAs()
1617  * @sa CMainFrame::CheckSavePath()
1618  * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1619  */
1620 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1621 {
1622         String strSavePath(szPath);
1623
1624         // use a temp packer
1625         // first copy the m_pInfoUnpacker
1626         // if an error arises during packing, change and take a "do nothing" packer
1627         PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1628
1629         bSaveSuccess = false;
1630         // false as long as the user is not satisfied
1631         // true if saving succeeds, even with another filename, or if the user cancels
1632         bool result = false;
1633         // the error code from the latest save operation, 
1634         // or SAVE_DONE when the save succeeds
1635         // TODO: Shall we return this code in addition to bSaveSuccess ?
1636         int nSaveErrorCode = SAVE_DONE;
1637
1638         // Use SAVE_NO_FILENAME to prevent asking about error
1639         nSaveErrorCode = SAVE_NO_FILENAME;
1640
1641         // Loop until user succeeds saving or cancels
1642         String sError;
1643         do
1644                 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1645         while (!result);
1646
1647         // Saving succeeded with given/selected filename
1648         if (nSaveErrorCode == SAVE_DONE)
1649         {
1650                 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1651                 m_filePaths[nBuffer] = strSavePath;
1652                 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1653                 UpdateHeaderPath(nBuffer);
1654                 bSaveSuccess = true;
1655                 result = true;
1656         }
1657         return result;
1658 }
1659
1660 /**
1661  * @brief Get left->right info for a moved line (apparent line number)
1662  */
1663 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1664 {
1665         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1666                 return -1;
1667
1668         int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1669         int realRightLine = -1;
1670         if (m_diffWrapper.GetDetectMovedBlocks())
1671         {
1672                 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1673                                 MovedLines::SIDE_RIGHT);
1674         }
1675         if (realRightLine != -1)
1676                 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1677         else
1678                 return -1;
1679 }
1680
1681 /**
1682  * @brief Get right->left info for a moved line (apparent line number)
1683  */
1684 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1685 {
1686         if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1687                 return -1;
1688
1689         int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1690         int realLeftLine = -1;
1691         if (m_diffWrapper.GetDetectMovedBlocks())
1692         {
1693                 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1694                                 MovedLines::SIDE_LEFT);
1695         }
1696         if (realLeftLine != -1)
1697                 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1698         else
1699                 return -1;
1700 }
1701
1702 /**
1703  * @brief Save modified documents.
1704  * This function asks if user wants to save modified documents. We also
1705  * allow user to cancel the closing.
1706  *
1707  * There is a special trick avoiding showing two save-dialogs, as MFC framework
1708  * sometimes calls this function twice. We use static counter for these calls
1709  * and if we already have saving in progress (counter == 1) we skip the new
1710  * saving dialog.
1711  *
1712  * @return true if docs are closed, false if closing is cancelled.
1713  */
1714 BOOL CMergeDoc::SaveModified()
1715 {
1716         static int counter;
1717         ++counter;
1718         if (counter > 1)
1719                 return false;
1720
1721         if (PromptAndSaveIfNeeded(true))
1722         {
1723                 counter = 0;
1724                 return true;
1725         }
1726         else
1727         {
1728                 counter = 0;
1729                 return false;
1730         }
1731 }
1732
1733 /**
1734  * @brief Sets the current difference.
1735  * @param [in] nDiff Difference to set as current difference.
1736  */
1737 void CMergeDoc::SetCurrentDiff(int nDiff)
1738 {
1739         if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1740                 m_nCurDiff = nDiff;
1741         else
1742                 m_nCurDiff = -1;
1743 }
1744
1745 /**
1746  * @brief Take care of rescanning document.
1747  * 
1748  * Update view and restore cursor and scroll position after
1749  * rescanning document.
1750  * @param [in] bForced If true rescan cannot be suppressed
1751  */
1752 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1753 {
1754         // Ignore suppressing when forced rescan
1755         if (!bForced)
1756                 if (!m_bEnableRescan) return;
1757
1758         CWaitCursor waitstatus;
1759
1760         CMergeEditView *pActiveView = GetActiveMergeView();
1761
1762         // store cursors and hide caret
1763         ForEachView([](auto& pView) { pView->PushCursors(); });
1764         pActiveView->HideCursor();
1765
1766         bool bBinary = false;
1767         IDENTLEVEL identical = IDENTLEVEL_NONE;
1768         int nRescanResult = Rescan(bBinary, identical, bForced);
1769
1770         // restore cursors and caret
1771         ForEachView([](auto& pView) { pView->PopCursors(); });
1772         pActiveView->ShowCursor();
1773
1774         ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1775                 // because of ghostlines, m_nTopLine may differ just after Rescan
1776                 // scroll both views to the same top line
1777                 pView->UpdateSiblingScrollPos(false);
1778
1779                 // make sure we see the cursor from the curent view
1780                 pView->EnsureVisible(pView->GetCursorPos());
1781         });
1782
1783         // Refresh display
1784         UpdateAllViews(nullptr);
1785
1786         // Show possible error after updating screen
1787         if (nRescanResult != RESCAN_SUPPRESSED)
1788                 ShowRescanError(nRescanResult, identical);
1789         m_LastRescan = COleDateTime::GetCurrentTime();
1790 }
1791
1792 /**
1793  * @brief Saves both files
1794  */
1795 void CMergeDoc::OnFileSave() 
1796 {
1797         // We will need to know if either of the originals actually changed
1798         // so we know whether to update the diff status
1799         bool bChangedOriginal = false;
1800
1801         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1802         {
1803                 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1804                 {
1805                         // (why we don't use return value of DoSave)
1806                         // DoSave will return true if it wrote to something successfully
1807                         // but we have to know if it overwrote the original file
1808                         bool bSaveOriginal = false;
1809                         DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1810                         if (bSaveOriginal)
1811                                 bChangedOriginal = true;
1812                 }
1813         }
1814
1815         // If either of the actual source files being compared was changed
1816         // we need to update status in the dir view
1817         if (bChangedOriginal)
1818         {
1819                 // If DirDoc contains diffs
1820                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1821                 {
1822                         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1823                         {
1824                                 if (m_bEditAfterRescan[nBuffer])
1825                                 {
1826                                         FlushAndRescan(false);
1827                                         break;
1828                                 }
1829                         }
1830
1831                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1832                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1833                                         m_nTrivialDiffs, bIdentical);
1834                 }
1835         }
1836 }
1837
1838 void CMergeDoc::DoFileSave(int nBuffer)
1839 {
1840         bool bSaveSuccess = false;
1841         bool bModified = false;
1842
1843         if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1844         {
1845                 bModified = true;
1846                 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1847         }
1848
1849         // If file were modified and saving succeeded,
1850         // update status on dir view
1851         if (bModified && bSaveSuccess)
1852         {
1853                 // If DirDoc contains compare results
1854                 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1855                 {
1856                         for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
1857                         {
1858                                 if (m_bEditAfterRescan[nBuffer1])
1859                                 {
1860                                         FlushAndRescan(false);
1861                                         break;
1862                                 }
1863                         }
1864
1865                         bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1866                         m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1867                                         m_nTrivialDiffs, bIdentical);
1868                 }
1869         }
1870 }
1871
1872 /**
1873  * @brief Saves left-side file
1874  */
1875 void CMergeDoc::OnFileSaveLeft()
1876 {
1877         DoFileSave(0);
1878 }
1879
1880 /**
1881  * @brief Saves middle-side file
1882  */
1883 void CMergeDoc::OnFileSaveMiddle()
1884 {
1885         DoFileSave(1);
1886 }
1887
1888 /**
1889  * @brief Saves right-side file
1890  */
1891 void CMergeDoc::OnFileSaveRight()
1892 {
1893         DoFileSave(m_nBuffers - 1);
1894 }
1895
1896 /**
1897  * @brief Saves left-side file with name asked
1898  */
1899 void CMergeDoc::OnFileSaveAsLeft()
1900 {
1901         bool bSaveResult = false;
1902         DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
1903 }
1904
1905 /**
1906  * @brief Called when "Save middle (as...)" item is updated
1907  */
1908 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
1909 {
1910         pCmdUI->Enable(m_nBuffers == 3);
1911 }
1912
1913 /**
1914  * @brief Saves right-side file with name asked
1915  */
1916 void CMergeDoc::OnFileSaveAsMiddle()
1917 {
1918         bool bSaveResult = false;
1919         DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
1920 }
1921
1922 /**
1923  * @brief Saves right-side file with name asked
1924  */
1925 void CMergeDoc::OnFileSaveAsRight()
1926 {
1927         bool bSaveResult = false;
1928         DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
1929 }
1930
1931 /**
1932  * @brief Update diff-number pane text in file compare.
1933  * The diff number pane shows selected difference/amount of differences when
1934  * there is difference selected. If there is no difference selected, then
1935  * the panel shows amount of differences. If there are ignored differences,
1936  * those are not count into numbers.
1937  * @param [in] pCmdUI UI component to update.
1938  */
1939 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI) 
1940 {
1941         TCHAR sIdx[32] = { 0 };
1942         TCHAR sCnt[32] = { 0 };
1943         String s;
1944         const int nDiffs = m_diffList.GetSignificantDiffs();
1945         
1946         // Files are identical - show text "Identical"
1947         if (nDiffs <= 0)
1948                 s = _("Identical");
1949         
1950         // There are differences, but no selected diff
1951         // - show amount of diffs
1952         else if (GetCurrentDiff() < 0)
1953         {
1954                 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
1955                 _itot_s(nDiffs, sCnt, 10);
1956                 strutils::replace(s, _T("%1"), sCnt);
1957         }
1958         
1959         // There are differences and diff selected
1960         // - show diff number and amount of diffs
1961         else
1962         {
1963                 s = _("Difference %1 of %2");
1964                 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
1965                 _itot_s(signInd + 1, sIdx, 10);
1966                 strutils::replace(s, _T("%1"), sIdx);
1967                 _itot_s(nDiffs, sCnt, 10);
1968                 strutils::replace(s, _T("%2"), sCnt);
1969         }
1970         pCmdUI->SetText(s.c_str());
1971 }
1972
1973 /**
1974  * @brief Update plugin name
1975  * @param [in] pCmdUI UI component to update.
1976  */
1977 void CMergeDoc::OnUpdatePluginName(CCmdUI* pCmdUI)
1978 {
1979         String pluginNames;
1980         if (m_pInfoUnpacker && !m_pInfoUnpacker->m_PluginName.empty())
1981                 pluginNames += m_pInfoUnpacker->m_PluginName + _T("&");
1982         PrediffingInfo prediffer;
1983         GetPrediffer(&prediffer);
1984         if (!prediffer.m_PluginName.empty())
1985                 pluginNames += prediffer.m_PluginName + _T("&");
1986         pCmdUI->SetText(pluginNames.substr(0, pluginNames.length() - 1).c_str());
1987 }
1988
1989 /**
1990  * @brief Change number of diff context lines
1991  */
1992 void CMergeDoc::OnDiffContext(UINT nID)
1993 {
1994         switch (nID)
1995         {
1996         case ID_VIEW_DIFFCONTEXT_0:
1997                 m_nDiffContext = 0; break;
1998         case ID_VIEW_DIFFCONTEXT_1:
1999                 m_nDiffContext = 1; break;
2000         case ID_VIEW_DIFFCONTEXT_3:
2001                 m_nDiffContext = 3; break;
2002         case ID_VIEW_DIFFCONTEXT_5:
2003                 m_nDiffContext = 5; break;
2004         case ID_VIEW_DIFFCONTEXT_7:
2005                 m_nDiffContext = 7; break;
2006         case ID_VIEW_DIFFCONTEXT_9:
2007                 m_nDiffContext = 9; break;
2008         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2009                 m_nDiffContext = -m_nDiffContext - 1; break;
2010         case ID_VIEW_DIFFCONTEXT_ALL:
2011                 if (m_nDiffContext >= 0)
2012                         m_nDiffContext = -m_nDiffContext - 1;
2013                 break;
2014         }
2015         GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2016         FlushAndRescan(true);
2017 }
2018
2019 /**
2020  * @brief Update number of diff context lines
2021  */
2022 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2023 {
2024         bool bCheck;
2025         switch (pCmdUI->m_nID)
2026         {
2027         case ID_VIEW_DIFFCONTEXT_0:
2028                 bCheck = (m_nDiffContext == 0); break;
2029         case ID_VIEW_DIFFCONTEXT_1:
2030                 bCheck = (m_nDiffContext == 1); break;
2031         case ID_VIEW_DIFFCONTEXT_3:
2032                 bCheck = (m_nDiffContext == 3); break;
2033         case ID_VIEW_DIFFCONTEXT_5:
2034                 bCheck = (m_nDiffContext == 5); break;
2035         case ID_VIEW_DIFFCONTEXT_7:
2036                 bCheck = (m_nDiffContext == 7); break;
2037         case ID_VIEW_DIFFCONTEXT_9:
2038                 bCheck = (m_nDiffContext == 9); break;
2039         case ID_VIEW_DIFFCONTEXT_TOGGLE:
2040                 bCheck = false; break;
2041         default:
2042                 bCheck = (m_nDiffContext < 0); break;
2043         }
2044         pCmdUI->SetCheck(bCheck);
2045         pCmdUI->Enable(true);
2046 }
2047
2048 /**
2049  * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2050  *
2051  * @note After PrimeTextBuffers(), all buffers should have the same length.
2052  */
2053 void CMergeDoc::PrimeTextBuffers()
2054 {
2055         SetCurrentDiff(-1);
2056         m_nTrivialDiffs = 0;
2057         int nDiff;
2058         int nDiffCount = m_diffList.GetSize();
2059         int file;
2060
2061         // walk the diff list and calculate numbers of extra lines to add
2062         int extras[3] = {0, 0, 0};   // extra lines added to each view
2063         m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2064
2065         // resize m_aLines once for each view
2066         UINT lcount[3] = {0, 0, 0};
2067         UINT lcountnew[3] = {0, 0, 0};
2068         UINT lcountmax = 0;
2069         
2070         for (file = 0; file < m_nBuffers; file++)
2071         {
2072                 lcount[file] = m_ptBuf[file]->GetLineCount();
2073                 lcountnew[file] = lcount[file] + extras[file];
2074                 lcountmax = max(lcountmax, lcountnew[file]);
2075         }
2076         for (file = 0; file < m_nBuffers; file++)
2077         {
2078                 m_ptBuf[file]->m_aLines.resize(lcountmax);
2079         }
2080
2081         // walk the diff list backward, move existing lines to proper place,
2082         // add ghost lines, and set flags
2083         for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2084         {
2085                 DIFFRANGE curDiff;
2086                 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2087
2088                 // move matched lines after curDiff
2089                 int nline[3] = { 0, 0, 0 };
2090                 for (file = 0; file < m_nBuffers; file++)
2091                         nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2092                 // Matched lines should really match...
2093                 // But matched lines after last diff may differ because of empty last line (see function's note)
2094                 if (nDiff < nDiffCount - 1)
2095                         ASSERT(nline[0] == nline[1]);
2096
2097                 int nmaxline = 0;
2098                 for (file = 0; file < m_nBuffers; file++)
2099                 {
2100                         // Move all lines after current diff down as far as needed
2101                         // for any ghost lines we're about to insert
2102                         m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2103                         lcountnew[file] -= nline[file];
2104                         lcount[file] -= nline[file];
2105                         // move unmatched lines and add ghost lines
2106                         nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2107                         nmaxline = max(nmaxline, nline[file]);
2108                 }
2109
2110                 for (file = 0; file < m_nBuffers; file++)
2111                 {
2112                         DWORD dflag = LF_GHOST;
2113                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2114                                 dflag |= LF_SNP;
2115                         m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2116                         int nextra = nmaxline - nline[file];
2117                         if (nextra > 0)
2118                         {
2119                                 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2120                                 for (int i = 1; i <= nextra; i++)
2121                                         m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2122                         }
2123                         lcountnew[file] -= nmaxline;
2124
2125                         lcount[file] -= nline[file];
2126
2127                 }
2128                 // set dbegin, dend, blank, and line flags
2129                 curDiff.dbegin = lcountnew[0];
2130
2131                 switch (curDiff.op)
2132                 {
2133                 case OP_TRIVIAL:
2134                         ++m_nTrivialDiffs;
2135                         // fall through and handle as diff
2136                 case OP_DIFF:
2137                 case OP_1STONLY:
2138                 case OP_2NDONLY:
2139                 case OP_3RDONLY:
2140                         // set curdiff
2141                         {
2142                                 curDiff.dend = lcountnew[0]+nmaxline-1;
2143                                 for (file = 0; file < m_nBuffers; file++)
2144                                 {
2145                                         curDiff.blank[file] = -1;
2146                                         int nextra = nmaxline - nline[file];
2147                                         if (nmaxline > nline[file])
2148                                         {
2149                                                 // more lines on left, ghost lines on right side
2150                                                 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2151                                         }
2152                                 }
2153                         }
2154                         // flag lines
2155                         {
2156                                 for (file = 0; file < m_nBuffers; file++)
2157                                 {
2158                                         // left side
2159                                         int i;
2160                                         for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2161                                         {
2162                                                 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2163                                                 {
2164                                                         // set diff or trivial flag
2165                                                         DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2166                                                         if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2167                                                                 dflag |= LF_SNP;
2168                                                         m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2169                                                         m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2170                                                 }
2171                                                 else
2172                                                 {
2173                                                         // ghost lines are already inserted (and flagged)
2174                                                         // ghost lines opposite to trivial lines are ghost and trivial
2175                                                         if (curDiff.op == OP_TRIVIAL)
2176                                                                 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2177                                                 }
2178                                         }
2179                                 }
2180                         }
2181                         break;
2182                 }           // switch (curDiff.op)
2183                 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2184         }             // for (nDiff = nDiffCount; nDiff-- > 0; )
2185
2186         m_diffList.ConstructSignificantChain();
2187
2188 #ifdef _DEBUG
2189         // Note: By this point all `m_ptBuf[]` buffers must have the same  
2190         //              number of line entries; eventual buffer processing typically
2191         //              uses the line count from `m_ptBuf[0]` for all buffer processing.
2192
2193         for (file = 0; file < m_nBuffers; file++)
2194         {
2195                 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2196         }
2197 #endif
2198
2199         for (file = 0; file < m_nBuffers; file++)
2200                 m_ptBuf[file]->FinishLoading();
2201 }
2202
2203 /**
2204  * @brief Checks if file has changed since last update (save or rescan).
2205  * @param [in] szPath File to check
2206  * @param [in] dfi Previous fileinfo of file
2207  * @param [in] bSave If true Compare to last save-info, else to rescan-info
2208  * @param [in] nBuffer Index (0-based) of buffer
2209  * @return true if file is changed.
2210  */
2211 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2212         bool bSave, int nBuffer)
2213 {
2214         DiffFileInfo *fileInfo = nullptr;
2215         bool bFileChanged = false;
2216         bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2217         int tolerance = 0;
2218         if (bIgnoreSmallDiff)
2219                 tolerance = SmallTimeDiff; // From MainFrm.h
2220
2221         if (bSave)
2222                 fileInfo = m_pSaveFileInfo[nBuffer].get();
2223         else
2224                 fileInfo = m_pRescanFileInfo[nBuffer].get();
2225
2226         // We assume file existed, so disappearing means removal
2227         if (!dfi.Update(szPath))
2228                 return FileRemoved;
2229
2230         int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2231         if (timeDiff < 0) timeDiff = -timeDiff;
2232         if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2233         {
2234                 bFileChanged = true;
2235         }
2236
2237         if (bFileChanged)
2238                 return FileChanged;
2239         else
2240                 return FileNoChange;
2241 }
2242
2243 void CMergeDoc::HideLines()
2244 {
2245         int nLine;
2246         int file;
2247
2248         if (m_nDiffContext < 0)
2249         {
2250                 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2251                 return;
2252         }
2253
2254         int nLineCount = 0x7fffffff;
2255         for (file = 0; file < m_nBuffers; file++)
2256         {
2257                 if (nLineCount > m_ptBuf[file]->GetLineCount())
2258                         nLineCount = m_ptBuf[file]->GetLineCount();
2259         }
2260
2261         for (nLine =  0; nLine < nLineCount;)
2262         {
2263                 if (!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST)))
2264                 {
2265                         for (file = 0; file < m_nBuffers; file++)
2266                                 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2267                         nLine++;
2268                 }
2269                 else
2270                 {
2271                         int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2272                         for (; nLine2 < nLine; nLine2++)
2273                         {
2274                                 for (file = 0; file < m_nBuffers; file++)
2275                                         m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2276                         }
2277                 
2278                         for (; nLine < nLineCount; nLine++)
2279                         {
2280                                 if (!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST)))
2281                                         break;
2282                                 for (file = 0; file < m_nBuffers; file++)
2283                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2284                         }
2285
2286                         int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2287                         for (; nLine < nLineEnd2; nLine++)
2288                         {
2289                                 for (file = 0; file < m_nBuffers; file++)
2290                                         m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2291                                 if (m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST))
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 = CRLF_STYLE_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] = BUFFER_NORMAL;
2596                 else
2597                         m_nBufferType[index] = BUFFER_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] = BUFFER_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, BUFFER_UNNAMED) == m_nBuffers)
2754                 {
2755                         m_pInfoUnpacker->Initialize(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] == BUFFER_NORMAL) ||
2921                             (m_nBufferType[nBuffer] == BUFFER_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] == BUFFER_UNNAMED ||
3039                 m_nBufferType[pane] == BUFFER_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;
3120         String sFileName[3];
3121
3122         if (lpszTitle != nullptr)
3123                 sTitle = lpszTitle;
3124         else
3125         {
3126                 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3127                         sFileName[nBuffer] = !m_strDesc[nBuffer].empty() ? m_strDesc[nBuffer] : paths::FindFileName(m_filePaths[nBuffer]);
3128                 if (std::count(&sFileName[0], &sFileName[0] + m_nBuffers, sFileName[0]) == m_nBuffers)
3129                         sTitle = sFileName[0] + strutils::format(_T(" x %d"), m_nBuffers);
3130                 else
3131                         sTitle = strutils::join(&sFileName[0], &sFileName[0] + m_nBuffers, _T(" - "));
3132         }
3133         CDocument::SetTitle(sTitle.c_str());
3134 }
3135
3136 /**
3137  * @brief Update any resources necessary after a GUI language change
3138  */
3139 void CMergeDoc::UpdateResources()
3140 {
3141         if (m_nBufferType[0] == BUFFER_UNNAMED)
3142                 m_strDesc[0] = _("Untitled left");
3143         if (m_nBufferType[m_nBuffers - 1] == BUFFER_UNNAMED)
3144                 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3145         if (m_nBuffers == 3 && m_nBufferType[1] == BUFFER_UNNAMED)
3146                 m_strDesc[1] = _("Untitled middle");
3147         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3148                 UpdateHeaderPath(nBuffer);
3149
3150         GetParentFrame()->UpdateResources();
3151         ForEachView([](auto& pView) { pView->UpdateResources(); });
3152 }
3153
3154 // Return current word breaking break type setting (whitespace only or include punctuation)
3155 bool CMergeDoc::GetBreakType() const
3156 {
3157         bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3158         return breakType;
3159 }
3160
3161 // Return true to do line diff colors at the byte level (false to do them at word level)
3162 bool CMergeDoc::GetByteColoringOption() const
3163 {
3164         // color at byte level if 'break_on_words' option not set
3165         bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3166         return !breakWords;
3167 }
3168
3169 /// Swap files and update views
3170 void CMergeDoc::SwapFiles()
3171 {
3172         // Swap views
3173         for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3174         {
3175                 int nLeftViewId = m_pView[nGroup][0]->GetDlgCtrlID();
3176                 int nRightViewId = m_pView[nGroup][m_nBuffers - 1]->GetDlgCtrlID();
3177                 m_pView[nGroup][0]->SetDlgCtrlID(nRightViewId);
3178                 m_pView[nGroup][m_nBuffers - 1]->SetDlgCtrlID(nLeftViewId);
3179         }
3180
3181
3182         // Swap buffers and so on
3183         std::swap(m_ptBuf[0], m_ptBuf[m_nBuffers - 1]);
3184         for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3185                 std::swap(m_pView[nGroup][0], m_pView[nGroup][m_nBuffers - 1]);
3186         std::swap(m_pSaveFileInfo[0], m_pSaveFileInfo[m_nBuffers - 1]);
3187         std::swap(m_pRescanFileInfo[0], m_pRescanFileInfo[m_nBuffers - 1]);
3188         std::swap(m_nBufferType[0], m_nBufferType[m_nBuffers - 1]);
3189         std::swap(m_bEditAfterRescan[0], m_bEditAfterRescan[m_nBuffers - 1]);
3190         std::swap(m_strDesc[0], m_strDesc[m_nBuffers - 1]);
3191
3192         m_filePaths.Swap();
3193         m_diffList.Swap(0, m_nBuffers - 1);
3194         for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3195                 swap(m_pView[nGroup][0]->m_piMergeEditStatus, m_pView[nGroup][m_nBuffers - 1]->m_piMergeEditStatus);
3196
3197         ClearWordDiffCache();
3198
3199         for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3200         {
3201                 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3202                 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3203                         m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3204
3205                 // Update views
3206                 UpdateHeaderPath(nBuffer);
3207         }
3208         GetParentFrame()->UpdateSplitter();
3209         ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3210
3211         UpdateAllViews(nullptr);
3212 }
3213
3214 /**
3215  * @brief Display unpacker dialog to user & handle user's choices
3216  */
3217 bool CMergeDoc::OpenWithUnpackerDialog()
3218 {
3219         // let the user choose a handler
3220         CSelectUnpackerDlg dlg(m_filePaths[0], nullptr);
3221         // create now a new infoUnpacker to initialize the manual/automatic flag
3222         PackingInfo infoUnpacker(PLUGIN_AUTO);
3223         dlg.SetInitialInfoHandler(&infoUnpacker);
3224
3225         if (dlg.DoModal() == IDOK)
3226         {
3227                 infoUnpacker = dlg.GetInfoHandler();
3228                 Merge7zFormatMergePluginScope scope(&infoUnpacker);
3229                 if (HasZipSupport() && std::count_if(m_filePaths.begin(), m_filePaths.end(), ArchiveGuessFormat) == m_nBuffers)
3230                 {
3231                         DWORD dwFlags[3] = {FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU};
3232                         GetMainFrame()->DoFileOpen(&m_filePaths, dwFlags, m_strDesc, _T(""), 
3233                                 GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, _T(""), &infoUnpacker);
3234                         CloseNow();
3235                 }
3236                 else
3237                 {
3238                         SetUnpacker(&infoUnpacker);
3239                         OnFileReload();
3240                 }
3241                 return true;
3242         }
3243         else
3244         {
3245                 return false;
3246         }
3247 }
3248
3249 /**
3250  * @brief Reloads the opened files
3251  */
3252 void CMergeDoc::OnFileReload()
3253 {
3254         if (!PromptAndSaveIfNeeded(true))
3255                 return;
3256         
3257         FileLocation fileloc[3];
3258         bool bRO[3];
3259         for (int pane = 0; pane < m_nBuffers; pane++)
3260         {
3261                 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3262                 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3263                 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3264                 fileloc[pane].setPath(m_filePaths[pane]);
3265         }
3266         CPoint pt = GetActiveMergeView()->GetCursorPos();
3267         if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3268                 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3269 }
3270
3271 /**
3272  * @brief Display encodings to user
3273  */
3274 void CMergeDoc::OnFileEncoding()
3275 {
3276         DoFileEncodingDialog();
3277 }
3278
3279 void CMergeDoc::OnCtxtOpenWithUnpacker() 
3280 {
3281         OpenWithUnpackerDialog();
3282 }
3283
3284 void CMergeDoc::OnBnClickedFileEncoding()
3285 {
3286         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3287                 return;
3288         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3289         DoFileEncodingDialog();
3290 }
3291
3292 void CMergeDoc::OnBnClickedPlugin()
3293 {
3294         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3295                 return;
3296         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3297         OpenWithUnpackerDialog();
3298 }
3299
3300 void CMergeDoc::OnBnClickedHexView()
3301 {
3302         OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3303 }
3304
3305 void CMergeDoc::OnOK()
3306 {
3307         if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3308                 return;
3309         m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3310 }
3311
3312 void CMergeDoc::OnFileRecompareAsText()
3313 {
3314         m_bEnableTableEditing = false;
3315         PackingInfo infoUnpacker;
3316         SetUnpacker(&infoUnpacker);
3317         OnFileReload();
3318 }
3319
3320 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3321 {
3322         pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode == PLUGIN_BUILTIN_XML ||
3323                 m_ptBuf[0]->GetTableEditing());
3324 }
3325
3326 void CMergeDoc::OnFileRecompareAsTable()
3327 {
3328         m_bEnableTableEditing = true;
3329         PackingInfo infoUnpacker;
3330         SetUnpacker(&infoUnpacker);
3331         OnFileReload();
3332 }
3333
3334 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3335 {
3336         pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3337 }
3338
3339 void CMergeDoc::OnFileRecompareAsXML()
3340 {
3341         m_bEnableTableEditing = false;
3342         PackingInfo infoUnpacker(PLUGIN_BUILTIN_XML);
3343         SetUnpacker(&infoUnpacker);
3344         OnFileReload();
3345 }
3346
3347 void CMergeDoc::OnUpdateFileRecompareAsXML(CCmdUI *pCmdUI)
3348 {
3349         pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode != PLUGIN_BUILTIN_XML);
3350 }
3351
3352 void CMergeDoc::OnFileRecompareAs(UINT nID)
3353 {
3354         DWORD dwFlags[3] = { 0 };
3355         FileLocation fileloc[3];
3356         for (int pane = 0; pane < m_nBuffers; pane++)
3357         {
3358                 fileloc[pane].setPath(m_filePaths[pane]);
3359                 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3360         }
3361         if (m_pEncodingErrorBar!=nullptr && m_pEncodingErrorBar->IsWindowVisible())
3362                 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3363         if (nID == ID_MERGE_COMPARE_HEX)
3364                 GetMainFrame()->ShowHexMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3365         else
3366                 GetMainFrame()->ShowImgMergeDoc(m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3367         GetParentFrame()->ShowWindow(SW_RESTORE);
3368         GetParentFrame()->DestroyWindow();
3369 }
3370
3371 // Return file extension either from file name 
3372 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3373 {
3374         String sExt;
3375         paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3376         return sExt;
3377 }
3378
3379 /**
3380  * @brief Generate report from file compare results.
3381  */
3382 bool CMergeDoc::GenerateReport(const String& sFileName) const
3383 {
3384         // calculate HTML font size
3385         LOGFONT lf;
3386         CDC dc;
3387         dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3388         m_pView[0][0]->GetFont(lf);
3389         int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3390
3391         // create HTML report
3392         UniStdioFile file;
3393         if (!file.Open(sFileName, _T("wt")))
3394         {
3395                 String errMsg = GetSysError(GetLastError());
3396                 String msg = strutils::format_string1(
3397                         _("Error creating the report:\n%1"), errMsg);
3398                 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3399                 return false;
3400         }
3401
3402         file.SetCodepage(ucr::CP_UTF_8);
3403
3404         CString headerText =
3405                 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3406                 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3407                 _T("<html>\n")
3408                 _T("<head>\n")
3409                 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3410                 _T("<title>WinMerge File Compare Report</title>\n")
3411                 _T("<style type=\"text/css\">\n")
3412                 _T("<!--\n")
3413                 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3414                 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3415                 _T("tr { vertical-align: top; }\n")
3416                 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3417                 _T("%s")
3418                 _T("-->\n")
3419                 _T("</style>\n")
3420                 _T("</head>\n")
3421                 _T("<body>\n")
3422                 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3423                 _T("<thead>\n")
3424                 _T("<tr>\n");
3425         String header = 
3426                 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3427         file.WriteString(header);
3428
3429         // Get paths
3430         // If archive, use archive path + folder + filename inside archive
3431         // If desc text given, use it
3432         PathContext paths = m_filePaths;
3433         if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3434         {
3435                 for (int i = 0; i < paths.GetSize(); i++)
3436                         m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3437         }
3438         else
3439         {
3440                 for (int i = 0; i < paths.GetSize(); i++)
3441                 {
3442                         if (!m_strDesc[i].empty())
3443                                 paths[i] = m_strDesc[i];
3444                 }
3445         }
3446
3447         // titles
3448         int nBuffer;
3449         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3450         {
3451                 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3452                         (double)100 / m_nBuffers);
3453                 file.WriteString(data);
3454                 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3455                 file.WriteString(_T("</th>\n"));
3456         }
3457         file.WriteString(
3458                 _T("</tr>\n")
3459                 _T("</thead>\n")
3460                 _T("<tbody>\n"));
3461
3462         // write the body of the report
3463         int idx[3] = {0};
3464         int nLineCount[3] = {0};
3465         int nDiff = 0;
3466         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3467                 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3468
3469         for (;;)
3470         {
3471                 file.WriteString(_T("<tr>\n"));
3472                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3473                 {
3474                         for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3475                         {
3476                                 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3477                                         break;
3478                         }
3479                                 
3480                         if (idx[nBuffer] < nLineCount[nBuffer])
3481                         {
3482                                 // line number
3483                                 int iVisibleLineNumber = 0;
3484                                 String tdtag = _T("<td class=\"ln\">");
3485                                 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3486                                 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3487                                 {
3488                                         iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3489                                 }
3490                                 if (nBuffer == 0 &&
3491                                         (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3492                                         (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3493                                 {
3494                                         ++nDiff;
3495                                         if (iVisibleLineNumber > 0)
3496                                         {
3497                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3498                                                 iVisibleLineNumber = 0;
3499                                         }
3500                                         else
3501                                                 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3502                                 }
3503                                 if (iVisibleLineNumber > 0)
3504                                         tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3505                                 else
3506                                         tdtag += _T("</td>");
3507                                 file.WriteString(tdtag);
3508                                 // line content
3509                                 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3510                                 idx[nBuffer]++;
3511                         }
3512                         else
3513                                 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3514                         file.WriteString(_T("\n"));
3515                 }
3516                 file.WriteString(_T("</tr>\n"));
3517
3518                 bool bBorderLine = false;
3519                 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3520                 {
3521                         if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3522                                 bBorderLine = true;
3523                 }
3524
3525                 if (bBorderLine)
3526                 {
3527                         file.WriteString(_T("<tr height=1>"));
3528                         for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3529                         {
3530                                 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3531                                         file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3532                                 else
3533                                         file.WriteString(_T("<td></td><td></td>"));
3534                         }
3535                         file.WriteString(_T("</tr>\n"));
3536                 }
3537
3538                 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3539                         break;
3540         }
3541         file.WriteString(
3542                 _T("</tbody>\n")
3543                 _T("</table>\n")
3544                 _T("</body>\n")
3545                 _T("</html>\n"));
3546
3547         file.Close();
3548
3549         return true;
3550 }
3551
3552 /**
3553  * @brief Generate report from file compare results.
3554  */
3555 void CMergeDoc::OnToolsGenerateReport()
3556 {
3557         String s;
3558         CString folder;
3559
3560         if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3561                 return;
3562
3563         if (GenerateReport(s.c_str()))
3564                 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3565 }
3566
3567 /**
3568  * @brief Generate patch from files selected.
3569  *
3570  * Creates a patch from selected files in active directory compare, or
3571  * active file compare. Files in file compare must be saved before
3572  * creating a patch.
3573  */
3574 void CMergeDoc::OnToolsGeneratePatch()
3575 {
3576         // If there are changes in files, tell user to save them first
3577         if (IsModified())
3578         {
3579                 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3580                 return;
3581         }
3582
3583         CPatchTool patcher;
3584         patcher.AddFiles(m_filePaths.GetLeft(),
3585                         m_filePaths.GetRight());
3586         patcher.CreatePatch();
3587 }
3588
3589 /**
3590  * @brief Add synchronization point
3591  */
3592 void CMergeDoc::AddSyncPoint()
3593 {
3594         int nLine[3];
3595         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3596         {
3597                 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3598                 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3599         }
3600
3601         // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3602         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3603                 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3604                 {
3605                         LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3606                         return;
3607                 }
3608
3609         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3610                 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3611                         DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3612
3613         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3614                 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3615
3616         m_bHasSyncPoints = true;
3617
3618         ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3619
3620         FlushAndRescan(true);
3621 }
3622
3623 /**
3624  * @brief Delete a synchronization point
3625  */
3626 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3627 {
3628         const auto syncpoints = GetSyncPointList();     
3629         for (auto syncpnt : syncpoints)
3630         {
3631                 if (syncpnt[pane] == nLine)
3632                 {
3633                         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3634                                 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3635                 }
3636         }
3637
3638         if (syncpoints.size() == 1)
3639                 m_bHasSyncPoints = false;
3640
3641         if (bRescan)
3642                 FlushAndRescan(true);
3643         return true;
3644 }
3645
3646 /**
3647  * @brief Clear Synchronization points
3648  */
3649 void CMergeDoc::ClearSyncPoints()
3650 {
3651         if (!m_bHasSyncPoints)
3652                 return;
3653
3654         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3655         {
3656                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3657                 for (int nLine = 0; nLine < nLineCount; ++nLine)
3658                 {
3659                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3660                                 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3661                 }
3662         }
3663         
3664         m_bHasSyncPoints = false;
3665
3666         FlushAndRescan(true);
3667 }
3668
3669 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3670 {
3671         std::vector<std::vector<int> > list;
3672         if (!m_bHasSyncPoints)
3673                 return list;
3674         int idx[3] = {-1, -1, -1};
3675         std::vector<int> points(m_nBuffers);
3676         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3677                 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3678         for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3679         {
3680                 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3681                 for (int nLine = 0; nLine < nLineCount; ++nLine)
3682                 {
3683                         if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3684                         {
3685                                 idx[nBuffer]++;
3686                                 if (static_cast<int>(list.size()) <= idx[nBuffer])
3687                                         list.push_back(points);
3688                                 list[idx[nBuffer]][nBuffer] = nLine;
3689                         }
3690                 }
3691         }
3692         return list;
3693 }
3694