OSDN Git Service

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