OSDN Git Service

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