OSDN Git Service

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