OSDN Git Service

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