1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
10 * @brief Implementation file for CMergeDoc
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
28 #include "FileTransform.h"
32 #include "OptionsDef.h"
33 #include "DiffFileInfo.h"
34 #include "SaveClosingDlg.h"
35 #include "OpenTableDlg.h"
38 #include "OptionsMgr.h"
39 #include "OptionsDiffOptions.h"
40 #include "MergeLineFlags.h"
41 #include "FileOrFolderSelect.h"
42 #include "LineFiltersList.h"
43 #include "SubstitutionFiltersList.h"
45 #include "codepage_detect.h"
46 #include "SelectPluginDlg.h"
47 #include "EncodingErrorBar.h"
48 #include "MergeCmdLineInfo.h"
50 #include "Constants.h"
51 #include "Merge7zFormatMergePluginImpl.h"
53 #include "PatchTool.h"
56 #include "stringdiffs.h"
64 int CMergeDoc::m_nBuffersTemp = 2;
66 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
68 /////////////////////////////////////////////////////////////////////////////
71 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
73 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
74 //{{AFX_MSG_MAP(CMergeDoc)
76 ON_COMMAND(ID_FILE_SAVE, OnFileSave)
77 ON_UPDATE_COMMAND_UI(ID_FILE_SAVE, OnUpdateFileSave)
78 ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
79 ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_LEFT, OnUpdateFileSaveLeft)
80 ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
81 ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_MIDDLE, OnUpdateFileSaveMiddle)
82 ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
83 ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_RIGHT, OnUpdateFileSaveRight)
84 ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
85 ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
86 ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
87 ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
88 ON_COMMAND(ID_FILE_LEFT_READONLY, OnFileReadOnlyLeft)
89 ON_UPDATE_COMMAND_UI(ID_FILE_LEFT_READONLY, OnUpdateFileReadOnlyLeft)
90 ON_COMMAND(ID_FILE_MIDDLE_READONLY, OnFileReadOnlyMiddle)
91 ON_UPDATE_COMMAND_UI(ID_FILE_MIDDLE_READONLY, OnUpdateFileReadOnlyMiddle)
92 ON_COMMAND(ID_FILE_RIGHT_READONLY, OnFileReadOnlyRight)
93 ON_UPDATE_COMMAND_UI(ID_FILE_RIGHT_READONLY, OnUpdateFileReadOnlyRight)
94 ON_COMMAND(ID_RESCAN, OnFileReload)
95 ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
96 ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
97 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
98 ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
99 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
100 ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
101 ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
103 ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
104 ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
105 ON_COMMAND(ID_SWAPPANES_SWAP12, (OnViewSwapPanes<0, 1>))
106 ON_COMMAND(ID_SWAPPANES_SWAP23, (OnViewSwapPanes<1, 2>))
107 ON_COMMAND(ID_SWAPPANES_SWAP13, (OnViewSwapPanes<0, 2>))
108 ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
109 ON_COMMAND(ID_REFRESH, OnRefresh)
111 ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
112 ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
114 ON_COMMAND(ID_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
115 ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
116 ON_COMMAND_RANGE(ID_NO_PREDIFFER, ID_NO_PREDIFFER, OnPrediffer)
117 ON_COMMAND_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnPrediffer)
118 ON_UPDATE_COMMAND_UI(ID_NO_PREDIFFER, OnUpdatePrediffer)
119 ON_UPDATE_COMMAND_UI_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnUpdatePrediffer)
120 // Encoding Error dialog
121 ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
122 ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
123 ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
124 ON_COMMAND(IDOK, OnOK)
126 ON_UPDATE_COMMAND_UI(ID_STATUS_PANE0FILE_RO, OnUpdateStatusRO)
127 ON_UPDATE_COMMAND_UI(ID_STATUS_PANE1FILE_RO, OnUpdateStatusRO)
128 ON_UPDATE_COMMAND_UI(ID_STATUS_PANE2FILE_RO, OnUpdateStatusRO)
129 ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
133 /////////////////////////////////////////////////////////////////////////////
134 // CMergeDoc construction/destruction
137 * @brief Constructor.
139 CMergeDoc::CMergeDoc()
140 : m_bEnableRescan(true)
142 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
145 , m_pEncodingErrorBar(nullptr)
146 , m_bHasSyncPoints(false)
147 , m_bAutoMerged(false)
150 , m_bAutomaticRescan(false)
151 , m_CurrentPredifferID(0)
152 , m_bChangedSchemeManually(false)
154 DIFFOPTIONS options = {0};
156 m_nBuffers = m_nBuffersTemp;
157 m_filePaths.SetSize(m_nBuffers);
159 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
161 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
162 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
163 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
164 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
165 m_bEditAfterRescan[nBuffer] = false;
168 m_bEnableRescan = true;
169 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
171 // COleDateTime m_LastRescan
172 curUndo = undoTgt.begin();
173 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
174 m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
176 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
177 Options::DiffOptions::Load(GetOptionsMgr(), options);
179 m_diffWrapper.SetOptions(&options);
180 m_diffWrapper.SetPrediffer(nullptr);
186 * Informs associated dirdoc that mergedoc is closing.
188 CMergeDoc::~CMergeDoc()
190 if (m_pDirDoc != nullptr)
192 m_pDirDoc->MergeDocClosing(this);
198 * @brief Deleted data associated with doc before closing.
200 void CMergeDoc::DeleteContents ()
202 CDocument::DeleteContents ();
203 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
205 m_ptBuf[nBuffer]->FreeAll ();
206 m_tempFiles[nBuffer].Delete();
211 * @brief Called when new document is created.
213 * Initialises buffers.
215 BOOL CMergeDoc::OnNewDocument()
217 if (!CDocument::OnNewDocument())
220 SetTitle(_("File Comparison").c_str());
222 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
223 m_ptBuf[nBuffer]->InitNew ();
228 * @brief Return active merge edit view (or left one if neither active)
230 CMergeEditView * CMergeDoc::GetActiveMergeView()
232 CView * pActiveView = GetParentFrame()->GetActiveView();
233 CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
234 if (pMergeEditView == nullptr)
235 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
236 return pMergeEditView;
239 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
241 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
244 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
246 if (infoNewHandler != nullptr)
248 m_infoUnpacker = *infoNewHandler;
252 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
254 m_diffWrapper.SetPrediffer(infoPrediffer);
257 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
259 m_diffWrapper.GetPrediffer(infoPrediffer);
262 const PrediffingInfo* CMergeDoc::GetPrediffer() const
264 static PrediffingInfo infoPrediffer;
265 m_diffWrapper.GetPrediffer(&infoPrediffer);
266 return &infoPrediffer;
269 /////////////////////////////////////////////////////////////////////////////
270 // CMergeDoc serialization
272 void CMergeDoc::Serialize(CArchive& ar)
274 ASSERT(false); // we do not use CDocument serialization
277 /////////////////////////////////////////////////////////////////////////////
278 // CMergeDoc commands
281 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
284 * original file is Ansi :
285 * buffer -> save as Ansi -> Ansi plugins -> diffutils
286 * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
287 * buffer -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
288 * (the plugins are optional, not the conversion)
289 * @todo Show SaveToFile() errors?
291 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
293 // and we don't repack the file
294 PackingInfo tempPacker(false);
296 // write buffer out to temporary file
298 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
299 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
303 * @brief Save files to temp files & compare again.
305 * @param bBinary [in,out] [in] If true, compare two binary files
306 * [out] If true binary file was detected.
307 * @param bIdentical [out] If true files were identical
308 * @param bForced [in] If true, suppressing is ignored and rescan
310 * @return Tells if rescan was successfully, was suppressed, or
312 * If this code is OK, Rescan has detached the views temporarily
313 * (positions of cursors have been lost)
314 * @note Rescan() ALWAYS compares temp files. Actual user files are not
315 * touched by Rescan().
316 * @sa CDiffWrapper::RunFileDiff()
318 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
319 bool bForced /* =false */)
321 DIFFOPTIONS diffOptions = {0};
322 DiffFileInfo fileInfo;
323 bool diffSuccess = false;
324 int nResult = RESCAN_OK;
325 FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
330 if (!m_bEnableRescan)
331 return RESCAN_SUPPRESSED;
334 ClearWordDiffCache();
336 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
338 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
342 m_diffWrapper.SetFilterList(_T(""));
345 if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
347 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
351 m_diffWrapper.SetSubstitutionList(nullptr);
354 if (GetView(0, 0)->m_CurSourceDef->type != 0)
355 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
357 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
359 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
361 // Check if files have been modified since last rescan
362 // Ignore checking in case of scratchpads (empty filenames)
363 if (!m_filePaths[nBuffer].empty())
365 Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
366 fileInfo, false, nBuffer);
369 m_LastRescan = COleDateTime::GetCurrentTime();
371 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
372 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
374 if (Changed[nBuffer] == FileChange::Removed)
376 String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
377 ShowMessageBox(msg, MB_ICONWARNING);
378 bool bSaveResult = false;
379 bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
380 if (!ok || !bSaveResult)
382 return RESCAN_FILE_ERR;
386 String temp = m_tempFiles[nBuffer].GetPath();
388 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
390 return RESCAN_TEMP_ERR;
395 String tempPath = env::GetTemporaryPath();
397 // Set up DiffWrapper
398 m_diffWrapper.GetOptions(&diffOptions);
403 m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
404 // Clear moved lines lists
405 if (m_diffWrapper.GetDetectMovedBlocks())
407 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
408 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
411 // Set paths for diffing and run diff
412 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
414 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
416 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
417 m_diffWrapper.SetCompareFiles(m_filePaths);
421 if (!HasSyncPoints())
423 // Save text buffer to file
424 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
426 m_ptBuf[nBuffer]->SetTempPath(tempPath);
427 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
430 m_diffWrapper.SetCreateDiffList(&m_diffList);
431 diffSuccess = m_diffWrapper.RunFileDiff();
434 m_diffWrapper.GetDiffStatus(&status);
435 if (bBinary) // believe caller if we were told these are binaries
436 status.bBinaries = true;
440 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();
441 int nStartLine[3] = {0};
442 int nLines[3], nRealLine[3];
443 for (size_t i = 0; i <= syncpoints.size(); ++i)
445 // Save text buffer to file
446 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
448 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
449 m_ptBuf[nBuffer]->SetTempPath(tempPath);
450 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(),
451 nStartLine[nBuffer], nLines[nBuffer]);
455 m_diffWrapper.SetCreateDiffList(&templist);
456 diffSuccess = m_diffWrapper.RunFileDiff();
457 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
458 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
460 // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
461 if (i == 0 && templist.GetSize() > 0)
462 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
463 if (nStartLine[nBuffer] == 0)
465 bool isEmptyFile = true;
466 for (int j = 0; j < nLines[nBuffer]; j++)
468 if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
477 templist.GetDiff(0, di);
478 if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
480 di.end[nBuffer] = -1;
481 templist.SetDiff(0, di);
486 m_diffList.AppendDiffList(templist, nRealLine);
487 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
488 nStartLine[nBuffer] += nLines[nBuffer];
491 DIFFSTATUS status_part;
492 m_diffWrapper.GetDiffStatus(&status_part);
493 if (bBinary) // believe caller if we were told these are binaries
494 status.bBinaries = true;
495 status.MergeStatus(status_part);
497 m_diffWrapper.SetCreateDiffList(&m_diffList);
500 // If one file has EOL before EOF and other not...
501 if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
503 // ..last DIFFRANGE of file which has EOL must be
504 // fixed to contain last line too
505 int lineCount[3] = { 0,0,0 };
506 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
507 lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
508 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
511 // set identical/diff result as recorded by diffutils
512 identical = status.Identical;
514 // Determine errors and binary file compares
516 nResult = RESCAN_FILE_ERR;
517 else if (status.bBinaries)
523 // Now update views and buffers for ghost lines
525 // Prevent displaying views during this update
526 // BTW, this solves the problem of double asserts
527 // (during the display of an assert message box, a second assert in one of the
528 // display functions happens, and hides the first assert)
529 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
531 // Remove blank lines and clear winmerge flags
532 // this operation does not change the modified flag
533 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
534 m_ptBuf[nBuffer]->prepareForRescan();
536 // Divide diff blocks to match lines.
537 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
540 // Analyse diff-list (updating real line-numbers)
541 // this operation does not change the modified flag
544 // Hide identical lines if diff-context is not 'All'
547 // Apply flags to lines that are trivial
548 PrediffingInfo infoPrediffer;
549 GetPrediffer(&infoPrediffer);
550 if (!infoPrediffer.GetPluginPipeline().empty())
553 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
554 if (m_diffWrapper.GetDetectMovedBlocks())
557 // After PrimeTextBuffers() we know amount of real diffs
558 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
560 // Identical files are also updated
561 if (!m_diffList.HasSignificantDiffs())
562 identical = IDENTLEVEL::ALL;
564 ForEachView([](auto& pView) {
565 // just apply some options to the views
566 pView->PrimeListWithFile();
567 // Now buffers data are valid
568 pView->ReAttachToBuffer();
570 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
572 m_bEditAfterRescan[nBuffer] = false;
576 if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
577 identical == IDENTLEVEL::ALL &&
578 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
579 [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
580 identical = IDENTLEVEL::NONE;
582 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
587 void CMergeDoc::CheckFileChanged(void)
590 DiffFileInfo fileInfo;
591 FileChange FileChange[3];
593 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
595 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
598 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
601 bool bDoReload = false;
602 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
604 if (FileChange[nBuffer] == FileChange::Changed)
606 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]);
607 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
614 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
616 if (FileChange[nBuffer] == FileChange::Changed)
618 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
619 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
625 /** @brief Apply flags to lines that are trivial */
626 void CMergeDoc::FlagTrivialLines(void)
628 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
630 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
633 for (int file = 0; file < m_nBuffers; ++file)
635 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
636 str[file] = p ? p : _T("");
639 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
641 DIFFOPTIONS diffOptions = {0};
642 m_diffWrapper.GetOptions(&diffOptions);
644 // Make the call to stringdiffs, which does all the hard & tedious computations
645 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
646 !diffOptions.bIgnoreCase,
647 !diffOptions.bIgnoreEol,
648 diffOptions.nIgnoreWhitespace,
649 GetBreakType(), // whitespace only or include punctuation
650 GetByteColoringOption());
651 if (!worddiffs.empty())
653 for (int file = 0; file < m_nBuffers; ++file)
654 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
661 /** @brief Adjust all different lines that were detected as actually matching moved lines */
662 void CMergeDoc::FlagMovedLines(void)
665 MovedLines *pMovedLines;
667 pMovedLines = m_diffWrapper.GetMovedLines(0);
668 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
670 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
673 TRACE(_T("%d->%d\n"), i, j);
675 // We only flag lines that are already marked as being different
676 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
677 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
679 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
680 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
682 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
683 if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
684 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
690 pMovedLines = m_diffWrapper.GetMovedLines(1);
691 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
693 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
696 TRACE(_T("%d->%d\n"), i, j);
698 // We only flag lines that are already marked as being different
699 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
700 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
702 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
703 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
705 int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
706 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
707 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
716 pMovedLines = m_diffWrapper.GetMovedLines(1);
717 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
719 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
722 TRACE(_T("%d->%d\n"), i, j);
724 // We only flag lines that are already marked as being different
725 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
726 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
728 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
729 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
731 int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
732 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
733 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
739 pMovedLines = m_diffWrapper.GetMovedLines(2);
740 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
742 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
745 TRACE(_T("%d->%d\n"), i, j);
747 // We only flag lines that are already marked as being different
748 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
749 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
751 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
752 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
754 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
755 if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
756 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
762 // todo: Need to record actual moved information
765 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
767 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
769 GetParentFrame()->InitialUpdateFrame(this, true);
770 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
772 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
776 * @brief Prints (error) message by rescan status.
778 * @param nRescanResult [in] Resultcocode from rescan().
779 * @param bIdentical [in] Were files identical?.
780 * @sa CMergeDoc::Rescan()
782 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
784 // Rescan was suppressed, there is no sensible status
785 if (nRescanResult == RESCAN_SUPPRESSED)
790 if (nRescanResult == RESCAN_FILE_ERR)
792 s = _("An error occurred while comparing the files.");
794 ShowMessageBox(s, MB_ICONSTOP);
798 if (nRescanResult == RESCAN_TEMP_ERR)
800 s = _("Temporary files could not be created. Check your temporary path settings.");
802 ShowMessageBox(s, MB_ICONSTOP);
806 // Files are not binaries, but they are identical
807 if (identical != IDENTLEVEL::NONE)
809 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
810 [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
814 bool CMergeDoc::Undo()
820 * @brief An instance of RescanSuppress prevents rescan during its lifetime
821 * (or until its Clear method is called, which ends its effect).
826 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
829 m_bPrev = doc.m_bEnableRescan;
830 m_doc.m_bEnableRescan = false;
837 m_doc.m_bEnableRescan = m_bPrev;
851 * @brief Copy all diffs from one side to side.
852 * @param [in] srcPane Source side from which diff is copied
853 * @param [in] dstPane Destination side
855 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
857 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
861 * @brief Copy range of diffs from one side to side.
862 * This function copies given range of differences from side to another.
863 * Ignored differences are skipped, and not copied.
864 * @param [in] srcPane Source side from which diff is copied
865 * @param [in] dstPane Destination side
866 * @param [in] firstDiff First diff copied (0-based index)
867 * @param [in] lastDiff Last diff copied (0-based index)
869 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
872 if (firstDiff > lastDiff)
873 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
875 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
876 if (lastDiff > m_diffList.GetSize() - 1)
877 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
880 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
881 firstDiff = max(0, firstDiff);
882 if (firstDiff > lastDiff)
885 RescanSuppress suppressRescan(*this);
887 // Note we don't care about m_nDiffs count to become zero,
888 // because we don't rescan() so it does not change
890 SetCurrentDiff(lastDiff);
892 bool bGroupWithPrevious = false;
893 if (firstWordDiff <= 0 && lastWordDiff == -1)
895 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
896 return; // sync failure
900 if (!WordListCopy(srcPane, dstPane, lastDiff,
901 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
902 return; // sync failure
905 SetEditedAfterRescan(dstPane);
907 int nGroup = GetActiveMergeView()->m_nThisGroup;
908 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
909 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
910 CPoint currentPosSrc = pViewSrc->GetCursorPos();
912 CPoint currentPosDst = pViewDst->GetCursorPos();
916 pViewDst->SetCursorPos(pt);
917 pViewDst->SetNewSelection(pt, pt, false);
918 pViewDst->SetNewAnchor(pt);
920 // copy from bottom up is more efficient
921 for (int i = lastDiff - 1; i >= firstDiff; --i)
923 if (m_diffList.IsDiffSignificant(i))
926 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
927 if (currentPosDst.y > pdi->dend)
929 if (pdi->blank[dstPane] >= 0)
930 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
931 else if (pdi->blank[srcPane] >= 0)
932 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
934 // Group merge with previous (merge undo data to one action)
935 bGroupWithPrevious = true;
936 if (i > firstDiff || firstWordDiff <= 0)
938 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
939 break; // sync failure
943 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
944 break; // sync failure
949 ForEachView(dstPane, [currentPosDst](auto& pView) {
950 pView->SetCursorPos(currentPosDst);
951 pView->SetNewSelection(currentPosDst, currentPosDst, false);
952 pView->SetNewAnchor(currentPosDst);
955 suppressRescan.Clear(); // done suppress Rescan
959 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
960 int firstLineDiff, int lastLineDiff)
962 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
963 firstDiff = max(0, firstDiff);
964 if (firstDiff > lastDiff)
967 RescanSuppress suppressRescan(*this);
969 bool bGroupWithPrevious = false;
970 if (firstLineDiff <= 0 && lastLineDiff == -1)
972 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
973 return; // sync failure
977 if (!PartialListCopy(srcPane, dstPane, lastDiff,
978 (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
979 return; // sync failure
983 SetEditedAfterRescan(dstPane);
985 int nGroup = GetActiveMergeView()->m_nThisGroup;
986 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
987 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
988 CPoint currentPosSrc = pViewSrc->GetCursorPos();
990 CPoint currentPosDst = pViewDst->GetCursorPos();
994 pViewDst->SetCursorPos(pt);
995 pViewDst->SetNewSelection(pt, pt, false);
996 pViewDst->SetNewAnchor(pt);
998 // copy from bottom up is more efficient
999 for (int i = lastDiff - 1; i >= firstDiff; --i)
1001 if (m_diffList.IsDiffSignificant(i))
1004 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1005 if (currentPosDst.y > pdi->dend)
1007 if (pdi->blank[dstPane] >= 0)
1008 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1009 else if (pdi->blank[srcPane] >= 0)
1010 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1012 // Group merge with previous (merge undo data to one action)
1013 bGroupWithPrevious = true;
1014 if (i > firstDiff || firstLineDiff <= 0)
1016 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1017 break; // sync failure
1021 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
1022 break; // sync failure
1027 ForEachView(dstPane, [currentPosDst](auto& pView) {
1028 pView->SetCursorPos(currentPosDst);
1029 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1030 pView->SetNewAnchor(currentPosDst);
1033 suppressRescan.Clear(); // done suppress Rescan
1037 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1039 template<class Type>
1040 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1042 bool equal_all = middle == left && middle == right && left == right;
1044 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1045 bool conflict = middle != left && middle != right && left != right;
1047 return std::pair<MergeResult, Type>(Conflict, left);
1052 return std::pair<MergeResult, Type>(Merged, middle);
1056 return std::pair<MergeResult, Type>(Merged, right);
1058 return std::pair<MergeResult, Type>(Merged, left);
1062 return std::pair<MergeResult, Type>(Merged, middle);
1065 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1069 * @brief Do auto-merge.
1070 * @param [in] dstPane Destination side
1072 void CMergeDoc::DoAutoMerge(int dstPane)
1074 const int lastDiff = m_diffList.GetSize() - 1;
1075 const int firstDiff = 0;
1076 bool bGroupWithPrevious = false;
1077 int autoMergedCount = 0;
1078 int unresolvedConflictCount = 0;
1080 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
1081 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1082 if (mergedEncoding.first == Merged)
1084 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1085 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1087 else if (mergedEncoding.first == Conflict)
1088 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1090 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
1091 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1092 if (mergedEOLStyle.first == Merged)
1094 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1095 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1097 else if (mergedEOLStyle.first == Conflict)
1098 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1100 RescanSuppress suppressRescan(*this);
1102 // Note we don't care about m_nDiffs count to become zero,
1103 // because we don't rescan() so it does not change
1105 SetCurrentDiff(lastDiff);
1107 SetEditedAfterRescan(dstPane);
1109 int nGroup = GetActiveMergeView()->m_nThisGroup;
1110 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1111 CPoint currentPosDst = pViewDst->GetCursorPos();
1112 currentPosDst.x = 0;
1115 pViewDst->SetCursorPos(pt);
1116 pViewDst->SetNewSelection(pt, pt, false);
1117 pViewDst->SetNewAnchor(pt);
1119 // copy from bottom up is more efficient
1120 for (int i = lastDiff; i >= firstDiff; --i)
1122 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1123 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1127 if (currentPosDst.y > pdi->dend)
1129 if (pdi->blank[dstPane] >= 0)
1130 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1131 else if (pdi->blank[srcPane] >= 0)
1132 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1134 // Group merge with previous (merge undo data to one action)
1135 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1136 break; // sync failure
1137 if (!bGroupWithPrevious)
1138 bGroupWithPrevious = true;
1141 if (pdi->op == OP_DIFF)
1142 ++unresolvedConflictCount;
1145 ForEachView(dstPane, [currentPosDst](auto& pView) {
1146 pView->SetCursorPos(currentPosDst);
1147 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1148 pView->SetNewAnchor(currentPosDst);
1151 suppressRescan.Clear(); // done suppress Rescan
1153 UpdateHeaderPath(dstPane);
1155 if (autoMergedCount > 0)
1156 m_bAutoMerged = true;
1158 // move to first conflict
1159 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1161 pViewDst->SelectDiff(nDiff, true, false);
1164 strutils::format_string2(
1165 _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1166 strutils::format(_T("%d"), autoMergedCount),
1167 strutils::format(_T("%d"), unresolvedConflictCount)),
1168 MB_ICONINFORMATION);
1172 * @brief Sanity check difference.
1174 * Checks that lines in difference are inside difference in both files.
1175 * If file is edited, lines added or removed diff lines get out of sync and
1176 * merging fails miserably.
1178 * @param [in] dr Difference to check.
1179 * @return true if difference lines match, false otherwise.
1181 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1183 const int cd_dbegin = dr.dbegin;
1184 const int cd_dend = dr.dend;
1186 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1188 // Must ensure line number is in range before getting line flags
1189 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1192 // Optimization - check last line first so we don't need to
1193 // check whole diff for obvious cases
1194 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1195 if (!(dwFlags & LF_WINMERGE_FLAGS))
1199 for (int line = cd_dbegin; line < cd_dend; line++)
1201 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1203 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1204 if (!(dwFlags & LF_WINMERGE_FLAGS))
1212 * @brief Copy selected (=current) difference from from side to side.
1213 * @param [in] srcPane Source side from which diff is copied
1214 * @param [in] dstPane Destination side
1215 * @param [in] nDiff Diff to copy, if -1 function determines it.
1216 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1217 * @return true if ok, false if sync failure & need to abort copy
1218 * previous action (allows one undo for copy all)
1220 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1221 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1223 int nGroup = GetActiveMergeView()->m_nThisGroup;
1224 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1225 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1226 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1228 // suppress Rescan during this method
1229 // (Not only do we not want to rescan a lot of times, but
1230 // it will wreck the line status array to rescan as we merge)
1231 RescanSuppress suppressRescan(*this);
1233 // If diff-number not given, determine it from active view
1236 nDiff = GetCurrentDiff();
1238 // No current diff, but maybe cursor is in diff?
1239 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1240 pViewDst->IsCursorInDiff()))
1242 // Find out diff under cursor
1243 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1244 nDiff = m_diffList.LineToDiff(ptCursor.y);
1251 VERIFY(m_diffList.GetDiff(nDiff, cd));
1252 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1253 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1254 bool bSrcWasMod = sbuf.IsModified();
1255 const int cd_dbegin = cd.dbegin;
1256 const int cd_dend = cd.dend;
1257 const int cd_blank = cd.blank[srcPane];
1258 bool bInSync = SanityCheckDiff(cd);
1262 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1263 return false; // abort copying
1266 // If we remove whole diff from current view, we must fix cursor
1267 // position first. Normally we would move to end of previous line,
1268 // but we want to move to begin of that line for usability.
1271 CPoint currentPos = pViewDst->GetCursorPos();
1273 if (currentPos.y > cd_dend)
1275 if (cd.blank[dstPane] >= 0)
1276 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1277 else if (cd.blank[srcPane] >= 0)
1278 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1280 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1283 // if the current diff contains missing lines, remove them from both sides
1284 int limit = cd_dend;
1286 // curView is the view which is changed, so the opposite of the source view
1287 dbuf.BeginUndoGroup(bGroupWithPrevious);
1290 // text was missing, so delete rest of lines on both sides
1291 // delete only on destination side since rescan will clear the other side
1292 if (cd_dend + 1 < dbuf.GetLineCount())
1294 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1298 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1299 ASSERT(cd_blank > 0);
1300 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1304 dbuf.FlushUndoGroup(pSource);
1305 dbuf.BeginUndoGroup(true);
1309 // copy the selected text over
1310 if (cd_dbegin <= limit)
1312 // text exists on left side, so just replace
1313 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1314 dbuf.FlushUndoGroup(pSource);
1315 dbuf.BeginUndoGroup(true);
1317 dbuf.FlushUndoGroup(pSource);
1322 // reset the mod status of the source view because we do make some
1323 // changes, but none that concern the source text
1324 sbuf.SetModified(bSrcWasMod);
1327 suppressRescan.Clear(); // done suppress Rescan
1332 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1333 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1335 int nGroup = GetActiveMergeView()->m_nThisGroup;
1336 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1337 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1338 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1340 // suppress Rescan during this method
1341 // (Not only do we not want to rescan a lot of times, but
1342 // it will wreck the line status array to rescan as we merge)
1343 RescanSuppress suppressRescan(*this);
1346 VERIFY(m_diffList.GetDiff(nDiff, cd));
1347 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1348 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1349 bool bSrcWasMod = sbuf.IsModified();
1350 const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1351 const int cd_dend = cd.dend;
1352 const int cd_blank = cd.blank[srcPane];
1353 bool bInSync = SanityCheckDiff(cd);
1357 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1358 return false; // abort copying
1361 // If we remove whole diff from current view, we must fix cursor
1362 // position first. Normally we would move to end of previous line,
1363 // but we want to move to begin of that line for usability.
1366 CPoint currentPos = pViewDst->GetCursorPos();
1368 if (currentPos.y > cd_dend)
1370 if (cd.blank[dstPane] >= 0)
1371 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1372 else if (cd.blank[srcPane] >= 0)
1373 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1375 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1378 // if the current diff contains missing lines, remove them from both sides
1379 int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1381 // curView is the view which is changed, so the opposite of the source view
1382 dbuf.BeginUndoGroup(bGroupWithPrevious);
1383 if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1385 // text was missing, so delete rest of lines on both sides
1386 // delete only on destination side since rescan will clear the other side
1387 if (limit+1 < dbuf.GetLineCount())
1389 dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1393 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1394 ASSERT(cd_dbegin > 0);
1395 dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1398 limit = cd_dbegin-1;
1399 dbuf.FlushUndoGroup(pSource);
1400 dbuf.BeginUndoGroup(true);
1403 // copy the selected text over
1404 if (cd_dbegin <= limit)
1406 // text exists on left side, so just replace
1407 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1408 dbuf.FlushUndoGroup(pSource);
1409 dbuf.BeginUndoGroup(true);
1411 dbuf.FlushUndoGroup(pSource);
1416 // reset the mod status of the source view because we do make some
1417 // changes, but none that concern the source text
1418 sbuf.SetModified(bSrcWasMod);
1420 suppressRescan.Clear(); // done suppress Rescan
1425 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1426 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1428 int nGroup = GetActiveMergeView()->m_nThisGroup;
1429 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1430 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1431 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1433 // suppress Rescan during this method
1434 // (Not only do we not want to rescan a lot of times, but
1435 // it will wreck the line status array to rescan as we merge)
1436 RescanSuppress suppressRescan(*this);
1439 VERIFY(m_diffList.GetDiff(nDiff, cd));
1440 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1441 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1442 bool bSrcWasMod = sbuf.IsModified();
1443 const int cd_dbegin = cd.dbegin;
1444 const int cd_dend = cd.dend;
1445 const int cd_blank = cd.blank[srcPane];
1446 bool bInSync = SanityCheckDiff(cd);
1450 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1451 return false; // abort copying
1454 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1456 if (worddiffs.empty())
1459 if (cd.end[srcPane] < cd.begin[srcPane])
1460 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1462 if (firstWordDiff == -1)
1464 if (lastWordDiff == -1)
1465 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1467 // If we remove whole diff from current view, we must fix cursor
1468 // position first. Normally we would move to end of previous line,
1469 // but we want to move to begin of that line for usability.
1472 CPoint currentPos = pViewDst->GetCursorPos();
1474 if (currentPos.y > cd_dend)
1476 if (cd.blank[dstPane] >= 0)
1477 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1478 else if (cd.blank[srcPane] >= 0)
1479 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1481 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1484 // if the current diff contains missing lines, remove them from both sides
1485 int limit = cd_dend;
1487 // curView is the view which is changed, so the opposite of the source view
1488 dbuf.BeginUndoGroup(bGroupWithPrevious);
1490 CString srcText, dstText;
1491 CPoint ptDstStart, ptDstEnd;
1492 CPoint ptSrcStart, ptSrcEnd;
1494 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1495 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1496 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1497 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1498 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1499 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1500 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1501 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1503 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1504 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1506 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1507 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1510 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1511 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1513 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1514 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1516 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1518 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1520 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1521 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1522 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1523 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1524 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1525 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1526 + dstText.Mid(dstEnd - ptDstStart.x);
1529 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1532 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1534 dbuf.FlushUndoGroup(pSource);
1536 // reset the mod status of the source view because we do make some
1537 // changes, but none that concern the source text
1538 sbuf.SetModified(bSrcWasMod);
1540 suppressRescan.Clear(); // done suppress Rescan
1547 * @brief Save file with new filename.
1549 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1550 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1551 * normal filename fails, to let user choose another filename/location.
1552 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1553 * using this function.
1554 * @param [in, out] strPath
1555 * - [in] : Initial path shown to user
1556 * - [out] : Path to new filename if saving succeeds
1557 * @param [in, out] nSaveResult
1558 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1560 * - [out] : Statuscode of this saving try
1561 * @param [in, out] sError Error string from lower level saving code
1562 * @param [in] nBuffer Buffer we are saving
1563 * @return false as long as the user is not satisfied. Calling function
1564 * should not continue until true is returned.
1565 * @sa CMergeDoc::DoSave()
1566 * @sa CMergeDoc::DoSaveAs()
1567 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1569 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1570 int nBuffer, PackingInfo& infoTempUnpacker)
1574 String strSavePath; // New path for next saving try
1577 int answer = IDOK; // Set default we use for scratchpads
1579 // We shouldn't get here if saving is succeed before
1580 ASSERT(nSaveResult != SAVE_DONE);
1582 // Select message based on reason function called
1583 if (nSaveResult == SAVE_PACK_FAILED)
1585 str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
1586 // replace the unpacker with a "do nothing" unpacker
1587 infoTempUnpacker.Initialize(false);
1591 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);
1594 // SAVE_NO_FILENAME is temporarily used for scratchpad.
1595 // So don't ask about saving in that case.
1596 if (nSaveResult != SAVE_NO_FILENAME)
1597 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1603 title = _("Save Left File As");
1604 else if (nBuffer == m_nBuffers - 1)
1605 title = _("Save Right File As");
1607 title = _("Save Middle File As");
1609 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1611 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1613 nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1616 if (nSaveResult == SAVE_DONE)
1618 // We are saving scratchpad (unnamed file)
1619 if (strPath.empty())
1621 m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1622 m_strDesc[nBuffer].erase();
1625 strPath = strSavePath;
1626 UpdateHeaderPath(nBuffer);
1632 nSaveResult = SAVE_CANCELLED;
1636 nSaveResult = SAVE_CANCELLED;
1643 * @brief Save file creating backups etc.
1645 * Safe top-level file saving function. Checks validity of given path.
1646 * Creates backup file if wanted to. And if saving to given path fails,
1647 * allows user to select new location/name for file.
1648 * @param [in] szPath Path where to save including filename. Can be
1649 * empty/`nullptr` if new file is created (scratchpad) without filename.
1650 * @param [out] bSaveSuccess Will contain information about save success with
1651 * the original name (to determine if file statuses should be changed)
1652 * @param [in] nBuffer Index (0-based) of buffer to save
1653 * @return Tells if caller can continue (no errors happened)
1654 * @note Return value does not tell if SAVING succeeded. Caller must
1655 * Check value of bSaveSuccess parameter after calling this function!
1656 * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1657 * to directory it points to. If m_strSaveAsPath contains filename,
1658 * that filename is used.
1659 * @sa CMergeDoc::TrySaveAs()
1660 * @sa CMainFrame::CheckSavePath()
1661 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1663 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1665 DiffFileInfo fileInfo;
1666 String strSavePath(szPath);
1667 FileChange fileChanged;
1668 bool bApplyToAll = false;
1671 fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1672 if (fileChanged == FileChange::Changed)
1674 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1675 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1677 bSaveSuccess = true;
1682 // use a temp packer
1683 // first copy the m_infoUnpacker
1684 // if an error arises during packing, change and take a "do nothing" packer
1685 PackingInfo infoTempUnpacker = m_infoUnpacker;
1687 bSaveSuccess = false;
1689 // Check third arg possibly given from command-line
1690 if (!theApp.m_strSaveAsPath.empty())
1692 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1694 // third arg was a directory, so get append the filename
1696 paths::SplitFilename(szPath, 0, &sname, 0);
1697 strSavePath = theApp.m_strSaveAsPath;
1698 strSavePath = paths::ConcatPath(strSavePath, sname);
1701 strSavePath = theApp.m_strSaveAsPath;
1704 nRetVal = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
1705 if (nRetVal == IDCANCEL)
1708 if (!CMergeApp::CreateBackup(false, strSavePath))
1711 // false as long as the user is not satisfied
1712 // true if saving succeeds, even with another filename, or if the user cancels
1713 bool result = false;
1714 // the error code from the latest save operation,
1715 // or SAVE_DONE when the save succeeds
1716 // TODO: Shall we return this code in addition to bSaveSuccess ?
1717 int nSaveErrorCode = SAVE_DONE;
1718 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1720 // Assume empty filename means Scratchpad (unnamed file)
1721 // Todo: This is not needed? - buffer type check should be enough
1722 if (strSavePath.empty())
1723 nSaveErrorCode = SAVE_NO_FILENAME;
1725 // Handle unnamed buffers
1726 if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1727 nSaveErrorCode = SAVE_NO_FILENAME;
1730 if (nSaveErrorCode == SAVE_DONE)
1731 // We have a filename, just try to save
1732 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, infoTempUnpacker);
1734 if (nSaveErrorCode != SAVE_DONE)
1736 // Saving failed, user may save to another location if wants to
1738 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1742 // Saving succeeded with given/selected filename
1743 if (nSaveErrorCode == SAVE_DONE)
1745 // Preserve file times if user wants to
1746 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1748 fileInfo.SetFile(strSavePath);
1751 TFile file(strSavePath);
1752 file.setLastModified(fileInfo.mtime);
1759 m_ptBuf[nBuffer]->SetModified(false);
1760 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1761 m_filePaths[nBuffer] = strSavePath;
1762 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1763 UpdateHeaderPath(nBuffer);
1764 bSaveSuccess = true;
1767 else if (nSaveErrorCode == SAVE_CANCELLED)
1769 // User cancelled current operation, lets do what user wanted to do
1776 * @brief Save file with different filename.
1778 * Safe top-level file saving function. Asks user to select filename
1779 * and path. Does not create backups.
1780 * @param [in] szPath Path where to save including filename. Can be
1781 * empty/`nullptr` if new file is created (scratchpad) without filename.
1782 * @param [out] bSaveSuccess Will contain information about save success with
1783 * the original name (to determine if file statuses should be changed)
1784 * @param [in] nBuffer Index (0-based) of buffer to save
1785 * @return Tells if caller can continue (no errors happened)
1786 * @note Return value does not tell if SAVING succeeded. Caller must
1787 * Check value of bSaveSuccess parameter after calling this function!
1788 * @sa CMergeDoc::TrySaveAs()
1789 * @sa CMainFrame::CheckSavePath()
1790 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1792 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1794 String strSavePath(szPath);
1796 // use a temp packer
1797 // first copy the m_infoUnpacker
1798 // if an error arises during packing, change and take a "do nothing" packer
1799 PackingInfo infoTempUnpacker = m_infoUnpacker;
1801 bSaveSuccess = false;
1802 // false as long as the user is not satisfied
1803 // true if saving succeeds, even with another filename, or if the user cancels
1804 bool result = false;
1805 // the error code from the latest save operation,
1806 // or SAVE_DONE when the save succeeds
1807 // TODO: Shall we return this code in addition to bSaveSuccess ?
1808 int nSaveErrorCode = SAVE_DONE;
1810 // Use SAVE_NO_FILENAME to prevent asking about error
1811 nSaveErrorCode = SAVE_NO_FILENAME;
1813 // Loop until user succeeds saving or cancels
1816 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, infoTempUnpacker);
1819 // Saving succeeded with given/selected filename
1820 if (nSaveErrorCode == SAVE_DONE)
1822 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1823 m_filePaths[nBuffer] = strSavePath;
1824 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1825 UpdateHeaderPath(nBuffer);
1826 bSaveSuccess = true;
1833 * @brief Get left->right info for a moved line (apparent line number)
1835 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1837 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1840 int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1841 int realRightLine = -1;
1842 if (m_diffWrapper.GetDetectMovedBlocks())
1844 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1845 MovedLines::SIDE::RIGHT);
1847 if (realRightLine != -1)
1848 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1854 * @brief Get right->left info for a moved line (apparent line number)
1856 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1858 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1861 int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1862 int realLeftLine = -1;
1863 if (m_diffWrapper.GetDetectMovedBlocks())
1865 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1866 MovedLines::SIDE::LEFT);
1868 if (realLeftLine != -1)
1869 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1875 * @brief Save modified documents.
1876 * This function asks if user wants to save modified documents. We also
1877 * allow user to cancel the closing.
1879 * There is a special trick avoiding showing two save-dialogs, as MFC framework
1880 * sometimes calls this function twice. We use static counter for these calls
1881 * and if we already have saving in progress (counter == 1) we skip the new
1884 * @return true if docs are closed, false if closing is cancelled.
1886 BOOL CMergeDoc::SaveModified()
1893 if (PromptAndSaveIfNeeded(true))
1906 * @brief Sets the current difference.
1907 * @param [in] nDiff Difference to set as current difference.
1909 void CMergeDoc::SetCurrentDiff(int nDiff)
1911 if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1918 * @brief Take care of rescanning document.
1920 * Update view and restore cursor and scroll position after
1921 * rescanning document.
1922 * @param [in] bForced If true rescan cannot be suppressed
1924 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1926 // Ignore suppressing when forced rescan
1928 if (!m_bEnableRescan) return;
1930 CWaitCursor waitstatus;
1932 CMergeEditView *pActiveView = GetActiveMergeView();
1934 // store cursors and hide caret
1935 ForEachView([](auto& pView) { pView->PushCursors(); });
1936 pActiveView->HideCursor();
1938 bool bBinary = false;
1939 IDENTLEVEL identical = IDENTLEVEL::NONE;
1940 int nRescanResult = Rescan(bBinary, identical, bForced);
1942 // restore cursors and caret
1943 ForEachView([](auto& pView) { pView->PopCursors(); });
1944 pActiveView->ShowCursor();
1946 ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1947 // because of ghostlines, m_nTopLine may differ just after Rescan
1948 // scroll both views to the same top line
1949 pView->UpdateSiblingScrollPos(false);
1951 // make sure we see the cursor from the curent view
1952 pActiveView->EnsureVisible(pActiveView->GetCursorPos());
1955 UpdateAllViews(nullptr);
1957 // Show possible error after updating screen
1958 if (nRescanResult != RESCAN_SUPPRESSED)
1959 ShowRescanError(nRescanResult, identical);
1960 m_LastRescan = COleDateTime::GetCurrentTime();
1964 * @brief Saves both files
1966 void CMergeDoc::OnFileSave()
1968 // We will need to know if either of the originals actually changed
1969 // so we know whether to update the diff status
1970 bool bChangedOriginal = false;
1972 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1974 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1976 // (why we don't use return value of DoSave)
1977 // DoSave will return true if it wrote to something successfully
1978 // but we have to know if it overwrote the original file
1979 bool bSaveOriginal = false;
1980 DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1982 bChangedOriginal = true;
1986 // If either of the actual source files being compared was changed
1987 // we need to update status in the dir view
1988 if (bChangedOriginal)
1990 // If DirDoc contains diffs
1991 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1993 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1995 if (m_bEditAfterRescan[nBuffer])
1997 FlushAndRescan(false);
2002 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2003 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2004 m_nTrivialDiffs, bIdentical);
2009 void CMergeDoc::DoFileSave(int nBuffer)
2011 bool bSaveSuccess = false;
2012 bool bModified = false;
2014 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
2017 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
2020 // If file were modified and saving succeeded,
2021 // update status on dir view
2022 if (bModified && bSaveSuccess)
2024 // If DirDoc contains compare results
2025 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2027 for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
2029 if (m_bEditAfterRescan[nBuffer1])
2031 FlushAndRescan(false);
2036 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2037 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2038 m_nTrivialDiffs, bIdentical);
2044 * @brief Saves left-side file
2046 void CMergeDoc::OnFileSaveLeft()
2052 * @brief Saves middle-side file
2054 void CMergeDoc::OnFileSaveMiddle()
2060 * @brief Saves right-side file
2062 void CMergeDoc::OnFileSaveRight()
2064 DoFileSave(m_nBuffers - 1);
2068 * @brief Called when "Save" item is updated
2070 void CMergeDoc::OnUpdateFileSave(CCmdUI* pCmdUI)
2072 bool bModified = false;
2073 for (int nPane = 0; nPane < m_nBuffers; nPane++)
2075 if (m_ptBuf[nPane]->IsModified())
2078 pCmdUI->Enable(bModified);
2082 * @brief Called when "Save left (as...)" item is updated
2084 void CMergeDoc::OnUpdateFileSaveLeft(CCmdUI* pCmdUI)
2086 pCmdUI->Enable(m_ptBuf[0]->IsModified());
2090 * @brief Called when "Save middle (as...)" item is updated
2092 void CMergeDoc::OnUpdateFileSaveMiddle(CCmdUI* pCmdUI)
2094 pCmdUI->Enable(m_nBuffers == 3 && m_ptBuf[1]->IsModified());
2098 * @brief Called when "Save right (as...)" item is updated
2100 void CMergeDoc::OnUpdateFileSaveRight(CCmdUI* pCmdUI)
2102 pCmdUI->Enable(m_ptBuf[m_nBuffers - 1]->IsModified());
2106 * @brief Saves left-side file with name asked
2108 void CMergeDoc::OnFileSaveAsLeft()
2110 bool bSaveResult = false;
2111 DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
2115 * @brief Called when "Save middle (as...)" item is updated
2117 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
2119 pCmdUI->Enable(m_nBuffers == 3);
2123 * @brief Saves right-side file with name asked
2125 void CMergeDoc::OnFileSaveAsMiddle()
2127 bool bSaveResult = false;
2128 DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
2132 * @brief Saves right-side file with name asked
2134 void CMergeDoc::OnFileSaveAsRight()
2136 bool bSaveResult = false;
2137 DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
2141 * @brief Enable/disable left buffer read-only
2143 void CMergeDoc::OnFileReadOnlyLeft()
2145 bool bReadOnly = m_ptBuf[0]->GetReadOnly();
2146 m_ptBuf[0]->SetReadOnly(!bReadOnly);
2150 * @brief Called when "Left read-only" item is updated
2152 void CMergeDoc::OnUpdateFileReadOnlyLeft(CCmdUI* pCmdUI)
2154 bool bReadOnly = m_ptBuf[0]->GetReadOnly();
2155 pCmdUI->Enable(true);
2156 pCmdUI->SetCheck(bReadOnly);
2160 * @brief Enable/disable middle buffer read-only
2162 void CMergeDoc::OnFileReadOnlyMiddle()
2164 if (m_nBuffers == 3)
2166 bool bReadOnly = m_ptBuf[1]->GetReadOnly();
2167 m_ptBuf[1]->SetReadOnly(!bReadOnly);
2172 * @brief Called when "Middle read-only" item is updated
2174 void CMergeDoc::OnUpdateFileReadOnlyMiddle(CCmdUI* pCmdUI)
2178 pCmdUI->Enable(false);
2182 bool bReadOnly = m_ptBuf[1]->GetReadOnly();
2183 pCmdUI->Enable(true);
2184 pCmdUI->SetCheck(bReadOnly);
2189 * @brief Enable/disable right buffer read-only
2191 void CMergeDoc::OnFileReadOnlyRight()
2193 bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
2194 m_ptBuf[m_nBuffers - 1]->SetReadOnly(!bReadOnly);
2198 * @brief Called when "Left read-only" item is updated
2200 void CMergeDoc::OnUpdateFileReadOnlyRight(CCmdUI* pCmdUI)
2202 bool bReadOnly = m_ptBuf[m_nBuffers - 1]->GetReadOnly();
2203 pCmdUI->Enable(true);
2204 pCmdUI->SetCheck(bReadOnly);
2208 * @brief Update readonly statusbaritem
2210 void CMergeDoc::OnUpdateStatusRO(CCmdUI* pCmdUI)
2212 bool bRO = m_ptBuf[pCmdUI->m_nID - ID_STATUS_PANE0FILE_RO]->GetReadOnly();
2213 pCmdUI->Enable(bRO);
2217 * @brief Update diff-number pane text in file compare.
2218 * The diff number pane shows selected difference/amount of differences when
2219 * there is difference selected. If there is no difference selected, then
2220 * the panel shows amount of differences. If there are ignored differences,
2221 * those are not count into numbers.
2222 * @param [in] pCmdUI UI component to update.
2224 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
2226 TCHAR sIdx[32] = { 0 };
2227 TCHAR sCnt[32] = { 0 };
2229 const int nDiffs = m_diffList.GetSignificantDiffs();
2231 // Files are identical - show text "Identical"
2235 // There are differences, but no selected diff
2236 // - show amount of diffs
2237 else if (GetCurrentDiff() < 0)
2239 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
2240 _itot_s(nDiffs, sCnt, 10);
2241 strutils::replace(s, _T("%1"), sCnt);
2244 // There are differences and diff selected
2245 // - show diff number and amount of diffs
2248 s = _("Difference %1 of %2");
2249 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
2250 _itot_s(signInd + 1, sIdx, 10);
2251 strutils::replace(s, _T("%1"), sIdx);
2252 _itot_s(nDiffs, sCnt, 10);
2253 strutils::replace(s, _T("%2"), sCnt);
2255 pCmdUI->SetText(s.c_str());
2259 * @brief Change number of diff context lines
2261 void CMergeDoc::OnDiffContext(UINT nID)
2265 case ID_VIEW_DIFFCONTEXT_0:
2266 m_nDiffContext = 0; break;
2267 case ID_VIEW_DIFFCONTEXT_1:
2268 m_nDiffContext = 1; break;
2269 case ID_VIEW_DIFFCONTEXT_3:
2270 m_nDiffContext = 3; break;
2271 case ID_VIEW_DIFFCONTEXT_5:
2272 m_nDiffContext = 5; break;
2273 case ID_VIEW_DIFFCONTEXT_7:
2274 m_nDiffContext = 7; break;
2275 case ID_VIEW_DIFFCONTEXT_9:
2276 m_nDiffContext = 9; break;
2277 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2278 m_nDiffContext = -m_nDiffContext - 1; break;
2279 case ID_VIEW_DIFFCONTEXT_ALL:
2280 if (m_nDiffContext >= 0)
2281 m_nDiffContext = -m_nDiffContext - 1;
2283 case ID_VIEW_DIFFCONTEXT_INVERT:
2284 m_bInvertDiffContext = !m_bInvertDiffContext;
2287 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2288 GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2289 FlushAndRescan(true);
2290 ForEachView([](auto& pView) { if (pView->m_bDetailView) pView->EnsureVisible(pView->GetCursorPos()); });
2294 * @brief Swap the positions of the two panes
2296 template<int srcPane, int dstPane>
2297 void CMergeDoc::OnViewSwapPanes()
2299 SwapFiles(srcPane, dstPane);
2303 * @brief Swap context enable for 3 file compares
2305 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2307 pCmdUI->Enable(m_nBuffers > 2);
2311 * @brief Update number of diff context lines
2313 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2316 switch (pCmdUI->m_nID)
2318 case ID_VIEW_DIFFCONTEXT_0:
2319 bCheck = (m_nDiffContext == 0); break;
2320 case ID_VIEW_DIFFCONTEXT_1:
2321 bCheck = (m_nDiffContext == 1); break;
2322 case ID_VIEW_DIFFCONTEXT_3:
2323 bCheck = (m_nDiffContext == 3); break;
2324 case ID_VIEW_DIFFCONTEXT_5:
2325 bCheck = (m_nDiffContext == 5); break;
2326 case ID_VIEW_DIFFCONTEXT_7:
2327 bCheck = (m_nDiffContext == 7); break;
2328 case ID_VIEW_DIFFCONTEXT_9:
2329 bCheck = (m_nDiffContext == 9); break;
2330 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2331 bCheck = false; break;
2332 case ID_VIEW_DIFFCONTEXT_INVERT:
2333 bCheck = m_bInvertDiffContext; break;
2335 bCheck = (m_nDiffContext < 0); break;
2337 pCmdUI->SetCheck(bCheck);
2338 pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2342 * @brief Refresh display using text-buffers
2343 * @note This DOES NOT reload files!
2345 void CMergeDoc::OnRefresh()
2347 FlushAndRescan(true);
2351 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2353 * @note After PrimeTextBuffers(), all buffers should have the same length.
2355 void CMergeDoc::PrimeTextBuffers()
2358 m_nTrivialDiffs = 0;
2360 int nDiffCount = m_diffList.GetSize();
2363 // walk the diff list and calculate numbers of extra lines to add
2364 int extras[3] = {0, 0, 0}; // extra lines added to each view
2365 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2367 // resize m_aLines once for each view
2368 UINT lcount[3] = {0, 0, 0};
2369 UINT lcountnew[3] = {0, 0, 0};
2372 for (file = 0; file < m_nBuffers; file++)
2374 lcount[file] = m_ptBuf[file]->GetLineCount();
2375 lcountnew[file] = lcount[file] + extras[file];
2376 lcountmax = max(lcountmax, lcountnew[file]);
2378 for (file = 0; file < m_nBuffers; file++)
2380 m_ptBuf[file]->m_aLines.resize(lcountmax);
2383 // walk the diff list backward, move existing lines to proper place,
2384 // add ghost lines, and set flags
2385 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2388 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2390 // move matched lines after curDiff
2391 int nline[3] = { 0, 0, 0 };
2392 for (file = 0; file < m_nBuffers; file++)
2393 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2394 // Matched lines should really match...
2395 // But matched lines after last diff may differ because of empty last line (see function's note)
2396 if (nDiff < nDiffCount - 1)
2397 ASSERT(nline[0] == nline[1]);
2400 for (file = 0; file < m_nBuffers; file++)
2402 // Move all lines after current diff down as far as needed
2403 // for any ghost lines we're about to insert
2404 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2405 lcountnew[file] -= nline[file];
2406 lcount[file] -= nline[file];
2407 // move unmatched lines and add ghost lines
2408 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2409 nmaxline = max(nmaxline, nline[file]);
2412 for (file = 0; file < m_nBuffers; file++)
2414 DWORD dflag = LF_GHOST;
2415 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2417 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2418 int nextra = nmaxline - nline[file];
2421 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2422 for (int i = 1; i <= nextra; i++)
2423 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2425 lcountnew[file] -= nmaxline;
2427 lcount[file] -= nline[file];
2430 // set dbegin, dend, blank, and line flags
2431 curDiff.dbegin = lcountnew[0];
2444 curDiff.dend = lcountnew[0]+nmaxline-1;
2445 for (file = 0; file < m_nBuffers; file++)
2447 curDiff.blank[file] = -1;
2448 int nextra = nmaxline - nline[file];
2449 if (nmaxline > nline[file])
2451 // more lines on left, ghost lines on right side
2452 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2458 for (file = 0; file < m_nBuffers; file++)
2462 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2464 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2466 // set diff or trivial flag
2467 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2468 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2470 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2471 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2475 // ghost lines are already inserted (and flagged)
2476 // ghost lines opposite to trivial lines are ghost and trivial
2477 if (curDiff.op == OP_TRIVIAL)
2478 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2484 } // switch (curDiff.op)
2485 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2486 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2488 m_diffList.ConstructSignificantChain();
2491 // Note: By this point all `m_ptBuf[]` buffers must have the same
2492 // number of line entries; eventual buffer processing typically
2493 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2495 for (file = 0; file < m_nBuffers; file++)
2497 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2501 for (file = 0; file < m_nBuffers; file++)
2502 m_ptBuf[file]->FinishLoading();
2506 * @brief Checks if file has changed since last update (save or rescan).
2507 * @param [in] szPath File to check
2508 * @param [in] dfi Previous fileinfo of file
2509 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2510 * @param [in] nBuffer Index (0-based) of buffer
2511 * @return true if file is changed.
2513 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2514 bool bSave, int nBuffer)
2516 DiffFileInfo *fileInfo = nullptr;
2517 bool bFileChanged = false;
2518 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2520 if (bIgnoreSmallDiff)
2521 tolerance = SmallTimeDiff; // From MainFrm.h
2524 fileInfo = m_pSaveFileInfo[nBuffer].get();
2526 fileInfo = m_pRescanFileInfo[nBuffer].get();
2528 // We assume file existed, so disappearing means removal
2529 if (!dfi.Update(szPath))
2530 return FileChange::Removed;
2532 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2533 if (timeDiff < 0) timeDiff = -timeDiff;
2534 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2536 bFileChanged = true;
2540 return FileChange::Changed;
2542 return FileChange::NoChange;
2545 void CMergeDoc::HideLines()
2550 if (m_nDiffContext < 0)
2552 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2556 int nLineCount = 0x7fffffff;
2557 for (file = 0; file < m_nBuffers; file++)
2559 if (nLineCount > m_ptBuf[file]->GetLineCount())
2560 nLineCount = m_ptBuf[file]->GetLineCount();
2563 for (nLine = 0; nLine < nLineCount;)
2565 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2566 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2568 for (file = 0; file < m_nBuffers; file++)
2569 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2574 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2575 for (; nLine2 < nLine; nLine2++)
2577 for (file = 0; file < m_nBuffers; file++)
2578 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2581 for (; nLine < nLineCount; nLine++)
2583 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2584 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2586 for (file = 0; file < m_nBuffers; file++)
2587 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2590 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2591 for (; nLine < nLineEnd2; nLine++)
2593 for (file = 0; file < m_nBuffers; file++)
2594 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2595 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2596 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2597 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2602 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2606 * @brief Asks and then saves modified files.
2608 * This function saves modified files. Dialog is shown for user to select
2609 * modified file(s) one wants to save or discard changed. Cancelling of
2610 * save operation is allowed unless denied by parameter. After successfully
2611 * save operation file statuses are updated to directory compare.
2612 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2613 * @return true if user selected "OK" so next operation can be
2614 * executed. If false user choosed "Cancel".
2615 * @note If filename is empty, we assume scratchpads are saved,
2616 * so instead of filename, description is shown.
2617 * @todo If we have filename and description for file, what should
2618 * we do after saving to different filename? Empty description?
2619 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2621 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2623 bool bLModified = false, bMModified = false, bRModified = false;
2625 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2627 if (m_nBuffers == 3)
2629 bLModified = m_ptBuf[0]->IsModified();
2630 bMModified = m_ptBuf[1]->IsModified();
2631 bRModified = m_ptBuf[2]->IsModified();
2635 bLModified = m_ptBuf[0]->IsModified();
2636 bRModified = m_ptBuf[1]->IsModified();
2638 if (!bLModified && !bMModified && !bRModified)
2642 dlg.DoAskFor(bLModified, bMModified, bRModified);
2644 dlg.m_bDisableCancel = true;
2645 if (!m_filePaths.GetLeft().empty())
2647 if (theApp.m_strSaveAsPath.empty())
2648 dlg.m_sLeftFile = m_filePaths.GetLeft();
2650 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2653 dlg.m_sLeftFile = m_strDesc[0];
2654 if (m_nBuffers == 3)
2656 if (!m_filePaths.GetMiddle().empty())
2658 if (theApp.m_strSaveAsPath.empty())
2659 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2661 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2664 dlg.m_sMiddleFile = m_strDesc[1];
2666 if (!m_filePaths.GetRight().empty())
2668 if (theApp.m_strSaveAsPath.empty())
2669 dlg.m_sRightFile = m_filePaths.GetRight();
2671 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2674 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2676 if (dlg.DoModal() == IDOK)
2678 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2680 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2684 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2686 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2690 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2692 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2701 // If file were modified and saving was successfull,
2702 // update status on dir view
2703 if ((bLModified && bLSaveSuccess) ||
2704 (bMModified && bMSaveSuccess) ||
2705 (bRModified && bRSaveSuccess))
2707 // If directory compare has results
2708 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2710 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2711 FlushAndRescan(false);
2713 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2714 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2715 m_nTrivialDiffs, bIdentical);
2722 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2723 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2725 // if we did not rescan during the request timeOut, Rescan
2726 // else we did Rescan after the request, so do nothing
2727 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2728 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2729 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2734 * @brief We have two child views (left & right), so we keep pointers directly
2735 * at them (the MFC view list doesn't have them both)
2737 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2740 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2741 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2745 void CMergeDoc::RemoveMergeViews(int nGroup)
2748 for (; nGroup < m_nGroups - 1; nGroup++)
2750 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2752 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2753 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2760 * @brief DirDoc gives us its identity just after it creates us
2762 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2764 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2765 m_pDirDoc = pDirDoc;
2769 * @brief Return pointer to parent frame
2771 CMergeEditFrame * CMergeDoc::GetParentFrame()
2773 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2777 * @brief DirDoc is closing
2779 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2781 ASSERT(m_pDirDoc == pDirDoc);
2782 m_pDirDoc = nullptr;
2783 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2787 * @brief DirDoc commanding us to close
2789 bool CMergeDoc::CloseNow()
2791 // Allow user to cancel closing
2792 if (!PromptAndSaveIfNeeded(true))
2795 GetParentFrame()->DestroyWindow();
2800 * @brief Loads file to buffer and shows load-errors
2801 * @param [in] sFileName File to open
2802 * @param [in] nBuffer Index (0-based) of buffer to load
2803 * @param [out] readOnly whether file is read-only
2804 * @param [in] encoding encoding used
2805 * @return Tells if files were loaded successfully
2806 * @sa CMergeDoc::OpenDocs()
2808 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2811 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2813 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2814 m_filePaths[nBuffer] = sFileName;
2816 CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2818 retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
2819 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2821 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2822 // it left the pBuf in a valid (but empty) state via a call to InitNew
2824 if (FileLoadResult::IsOkImpure(retVal))
2826 // File loaded, and multiple EOL types in this file
2827 FileLoadResult::SetMainOk(retVal);
2829 // If mixed EOLs are not enabled, enable them for this doc.
2830 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2832 pBuf->SetMixedEOL(true);
2836 if (FileLoadResult::IsError(retVal))
2838 // Error from Unifile/system
2839 if (!sOpenError.IsEmpty())
2840 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2842 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2843 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2845 else if (FileLoadResult::IsErrorUnpack(retVal))
2847 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2848 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2854 * @brief Check if specified codepage number is valid for WinMerge Editor.
2855 * @param [in] cp Codepage number to check.
2856 * @return true if codepage is valid, false otherwise.
2858 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2860 if (cp == 0) // 0 is our signal value for invalid
2862 return GetEncodingNameFromCodePage(cp) != nullptr;
2866 * @brief Sanity check file's specified codepage.
2867 * This function checks if file's specified codepage is valid for WinMerge
2868 * editor and if not resets the codepage to default.
2869 * @param [in,out] fileinfo Class containing file's codepage.
2871 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2873 if (fileinfo.encoding.m_unicoding == ucr::NONE
2874 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2876 int cp = ucr::getDefaultCodepage();
2877 if (!IsValidCodepageForMergeEditor(cp))
2879 fileinfo.encoding.SetCodepage(cp);
2884 * @brief Loads one file from disk and updates file infos.
2885 * @param [in] index Index of file in internal buffers.
2886 * @param [in] filename File's name.
2887 * @param [in] readOnly Is file read-only?
2888 * @param [in] encoding File's encoding.
2889 * @return One of FileLoadResult values.
2891 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2892 const FileTextEncoding & encoding)
2894 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2896 m_strDesc[index] = strDesc;
2897 if (!filename.empty())
2899 if (strDesc.empty())
2900 m_nBufferType[index] = BUFFERTYPE::NORMAL;
2902 m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2903 m_pSaveFileInfo[index]->Update(filename);
2904 m_pRescanFileInfo[index]->Update(filename);
2906 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2907 if (FileLoadResult::IsLossy(loadSuccess))
2909 // Determine the file encoding by looking at all the contents of the file, not just part of it
2910 FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
2911 if (encoding != encodingNew)
2913 m_ptBuf[index]->FreeAll();
2914 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
2920 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2921 m_ptBuf[index]->InitNew();
2922 m_ptBuf[index]->m_encoding = encoding;
2923 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2924 loadSuccess = FileLoadResult::FRESULT_OK;
2929 CMergeDoc::TableProps CMergeDoc::MakeTablePropertiesByFileName(const String& path, const std::optional<bool>& enableTableEditing, bool showDialog)
2931 const TCHAR quote = strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR));
2932 FileFilterHelper filterCSV, filterTSV, filterDSV;
2933 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2934 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2935 if (!csvFilePattern.empty())
2937 filterCSV.UseMask(true);
2938 filterCSV.SetMask(csvFilePattern);
2939 if (filterCSV.includeFile(path))
2940 return { true, ',', quote, allowNewlineIQuotes };
2942 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2943 if (!tsvFilePattern.empty())
2945 filterTSV.UseMask(true);
2946 filterTSV.SetMask(tsvFilePattern);
2947 if (filterTSV.includeFile(path))
2948 return { true, '\t', quote, allowNewlineIQuotes };
2950 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2951 if (!dsvFilePattern.empty())
2953 filterDSV.UseMask(true);
2954 filterDSV.SetMask(dsvFilePattern);
2955 if (filterDSV.includeFile(path))
2956 return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
2958 if (enableTableEditing.value_or(false))
2963 if (dlg.DoModal() == IDOK)
2964 return { true, strutils::from_charstr(dlg.m_sDelimiterChar), strutils::from_charstr(dlg.m_sQuoteChar), dlg.m_bAllowNewlinesInQuotes };
2968 return { true, strutils::from_charstr(GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR)), quote };
2971 return { false, 0, 0, false };
2974 void CMergeDoc::SetTableProperties()
2976 TableProps tableProps[3] = { };
2977 int nTableFileIndex = -1;
2978 if (m_pTablePropsPrepared)
2980 nTableFileIndex = 0;
2981 tableProps[0] = *m_pTablePropsPrepared;
2985 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2988 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2989 tableProps[nBuffer] = MakeTablePropertiesByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2991 tableProps[nBuffer] = tableProps[nBuffer - 1];
2992 if (tableProps[nBuffer].istable)
2993 nTableFileIndex = nBuffer;
2996 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2998 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
3000 int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
3001 m_ptBuf[nBuffer]->SetTableEditing(true);
3002 m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
3003 m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
3004 m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
3005 m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
3006 m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
3010 m_ptBuf[nBuffer]->SetTableEditing(false);
3015 void CMergeDoc::SetTextType(int textType)
3017 ForEachView([textType, this](auto& pView) {
3018 pView->SetTextType(CrystalLineParser::TextType(textType));
3019 pView->SetDisableBSAtSOL(false);
3020 m_bChangedSchemeManually = true;
3024 void CMergeDoc::SetTextType(const String& ext)
3027 strutils::replace(ext2, _T("."), _T(""));
3028 ForEachView([&ext2, this](auto& pView) {
3029 pView->SetTextType(ext2.c_str());
3030 pView->SetDisableBSAtSOL(false);
3031 m_bChangedSchemeManually = true;
3036 * @brief Loads files and does initial rescan.
3037 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
3038 * @param bRO [in] Is left/middle/right file read-only
3039 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
3040 * @todo Options are still read from CMainFrame, this will change
3041 * @sa CMainFrame::ShowTextMergeDoc()
3043 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
3044 const bool bRO[], const String strDesc[])
3046 CWaitCursor waitstatus;
3047 IDENTLEVEL identical = IDENTLEVEL::NONE;
3048 int nRescanResult = RESCAN_OK;
3050 FileLocation fileloc[3];
3052 std::copy_n(ifileloc, 3, fileloc);
3054 // Filter out invalid codepages, or editor will display all blank
3055 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3056 SanityCheckCodepage(fileloc[nBuffer]);
3060 curUndo = undoTgt.begin();
3062 // Prevent displaying views during LoadFile
3063 // Note : attach buffer again only if both loads succeed
3064 m_strBothFilenames.erase();
3066 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
3068 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3070 // clear undo buffers
3071 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
3074 m_ptBuf[nBuffer]->FreeAll ();
3076 // build the text being filtered, "|" separates files as it is forbidden in filenames
3077 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
3079 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
3082 DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR };
3083 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3085 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
3086 fileloc[nBuffer].encoding);
3087 if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
3089 CMergeEditFrame* pFrame = GetParentFrame();
3090 if (pFrame != nullptr)
3092 // Use verify macro to trap possible error in debug.
3093 VERIFY(pFrame->DestroyWindow());
3099 SetTableProperties();
3101 const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
3103 // scratchpad : we don't call LoadFile, so
3104 // we need to initialize the unpacker as a "do nothing" one
3105 if (bFiltersEnabled)
3107 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
3109 m_infoUnpacker.Initialize(false);
3113 // Warn user if file load was lossy (bad encoding)
3115 int nLossyBuffers = 0;
3116 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3118 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
3120 // TODO: It would be nice to report how many lines were lossy
3121 // we did calculate those numbers when we loaded the files, in the text stats
3123 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
3127 if (nLossyBuffers > 1)
3128 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
3130 if (nLossyBuffers > 0)
3132 if (m_pEncodingErrorBar == nullptr)
3134 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
3135 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
3137 m_pEncodingErrorBar->SetText(LoadResString(idres));
3138 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
3141 ForEachView([](auto& pView) {
3142 // Now buffers data are valid
3143 pView->AttachToBuffer();
3144 // Currently there is only one set of syntax colors, which all documents & views share
3145 pView->SetColorContext(theApp.GetMainSyntaxColors());
3146 // Currently there is only one set of markers, which all documents & views share
3147 pView->SetMarkersContext(theApp.GetMainMarkers());
3149 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3151 // Set read-only statuses
3152 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
3155 // Check the EOL sensitivity option (do it before Rescan)
3156 DIFFOPTIONS diffOptions = {0};
3157 m_diffWrapper.GetOptions(&diffOptions);
3158 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
3160 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3161 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
3164 if (nBuffer < m_nBuffers)
3166 // Options and files not are not compatible :
3167 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
3168 // All lines will differ, that is not very interesting and probably not wanted.
3169 // Propose to turn off the option 'sensitive to EOL'
3170 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
3171 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
3173 diffOptions.bIgnoreEol = true;
3174 m_diffWrapper.SetOptions(&diffOptions);
3176 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
3177 const int nFormerResult = dlg.GetFormerResult();
3178 if (nFormerResult != -1)
3180 // "Don't ask this question again" checkbox is checked
3181 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
3187 // Define the prediffer
3188 PackingInfo * infoUnpacker = nullptr;
3189 PrediffingInfo * infoPrediffer = nullptr;
3190 if (bFiltersEnabled && m_pDirDoc != nullptr)
3192 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3193 m_diffWrapper.SetPrediffer(infoPrediffer);
3194 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3197 bool bBinary = false;
3198 nRescanResult = Rescan(bBinary, identical);
3200 // Open filed if rescan succeed and files are not binaries
3201 if (nRescanResult == RESCAN_OK)
3203 // set the document types
3204 // Warning : it is the first thing to do (must be done before UpdateView,
3205 // or any function that calls UpdateView, like SelectDiff)
3206 // Note: If option enabled, and another side type is not recognized,
3207 // we use recognized type for unrecognized side too.
3212 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3214 sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
3215 ForEachView(nBuffer, [&](auto& pView) {
3216 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3217 if (bTyped[nBuffer])
3218 paneTyped = nBuffer;
3222 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3224 if (bTyped[0] != bTyped[nBuffer])
3228 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3229 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3231 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3234 m_ptBuf[0]->GetLine(0, sFirstLine);
3235 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3237 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3242 if (syntaxHLEnabled)
3244 CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3245 ForEachView([&bTyped, enuType](auto& pView) {
3246 if (!bTyped[pView->m_nThisPane])
3247 pView->SetTextType(enuType);
3251 int nNormalBuffer = 0;
3252 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3254 // set the frame window header
3255 UpdateHeaderPath(nBuffer);
3257 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3259 if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3260 (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3267 // Inform user that files are identical
3268 // Don't show message if new buffers created
3269 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3271 ShowRescanError(nRescanResult, identical);
3274 // Exit if files are identical should only work for the first
3275 // comparison and must be disabled afterward.
3276 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3280 // CMergeDoc::Rescan fails if files do not exist on both sides
3281 // or the really arcane case that the temp files couldn't be created,
3282 // which is too obscure to bother reporting if you can't write to
3283 // your temp directory, doing nothing is graceful enough for that).
3284 ShowRescanError(nRescanResult, identical);
3285 GetParentFrame()->DestroyWindow();
3289 // Force repaint of location pane to update it in case we had some warning
3290 // dialog visible and it got painted before files were loaded
3291 if (m_pView[0][0] != nullptr)
3292 m_pView[0][0]->RepaintLocationPane();
3297 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex, bool bRealLine, int nCharIndex)
3301 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3302 if (nPane < 0 || nPane >= m_nBuffers)
3305 if (nLineIndex == -1)
3307 // scroll to first diff
3308 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3309 m_diffList.HasSignificantDiffs())
3311 int nDiff = m_diffList.FirstSignificantDiff();
3313 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3314 m_pView[0][nPane]->SetActivePane();
3318 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, bRealLine, nPane, true, nCharIndex);
3321 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3323 if (!PromptAndSaveIfNeeded(true))
3326 FileLocation fileloc[3];
3329 for (int pane = 0; pane < m_nBuffers; pane++)
3331 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3332 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3333 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3334 fileloc[pane].setPath(m_filePaths[pane]);
3336 std::copy_n(m_strDesc, m_nBuffers, strDesc);
3338 strDesc[nBuffer] = _T("");
3339 fileloc[nBuffer].setPath(path);
3340 fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3342 bool filenameChanged = path != m_filePaths[nBuffer];
3343 auto columnWidths = m_ptBuf[nBuffer]->GetColumnWidths();
3345 if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3347 if (!filenameChanged)
3348 m_ptBuf[nBuffer]->SetColumnWidths(columnWidths);
3349 MoveOnLoad(nBuffer, nLineIndex);
3354 * @brief Re-load a document.
3355 * This methods re-loads the file compare document. The re-loaded document is
3356 * one side of the file compare.
3357 * @param [in] index The document to re-load.
3358 * @return Open result code.
3360 void CMergeDoc::RefreshOptions()
3362 DIFFOPTIONS options = {0};
3364 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3366 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3367 Options::DiffOptions::Load(GetOptionsMgr(), options);
3369 m_diffWrapper.SetOptions(&options);
3371 // Refresh view options
3372 ForEachView([](auto& pView) { pView->RefreshOptions(); });
3376 * @brief Write path and filename to headerbar
3377 * @note SetText() does not repaint unchanged text
3379 void CMergeDoc::UpdateHeaderPath(int pane)
3381 CMergeEditFrame *pf = GetParentFrame();
3382 ASSERT(pf != nullptr);
3384 bool bChanges = false;
3386 if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3387 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3389 sText = m_strDesc[pane];
3393 sText = m_filePaths[pane];
3394 if (m_pDirDoc != nullptr)
3396 m_pDirDoc->ApplyDisplayRoot(pane, sText);
3399 bChanges = m_ptBuf[pane]->IsModified();
3402 sText.insert(0, _T("* "));
3404 if (m_sCurrentHeaderTitle[pane] == sText)
3407 m_sCurrentHeaderTitle[pane] = sText;
3409 pf->GetHeaderInterface()->SetText(pane, sText);
3415 * @brief Paint differently the headerbar of the active view
3417 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3419 CMergeEditFrame *pf = GetParentFrame();
3420 ASSERT(pf != nullptr);
3421 pf->GetHeaderInterface()->SetActive(pane, bActivate);
3425 * @brief Set detect/not detect Moved Blocks
3427 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3429 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3432 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3433 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3438 * @brief Check if given buffer has mixed EOL style.
3439 * @param [in] nBuffer Buffer to check.
3440 * @return true if buffer's EOL style is mixed, false otherwise.
3442 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3444 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3445 return pBuf->IsMixedEOL();
3448 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3450 m_bEditAfterRescan[nBuffer] = true;
3453 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3455 if (nBuffer >= 0 && nBuffer < m_nBuffers)
3456 return m_bEditAfterRescan[nBuffer];
3458 for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3460 if (m_bEditAfterRescan[nBuffer])
3468 * @brief Update document filenames to title
3470 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3472 PrediffingInfo infoPrediffer;
3473 GetPrediffer(&infoPrediffer);
3474 String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer);
3475 CDocument::SetTitle(sTitle.c_str());
3479 * @brief Update any resources necessary after a GUI language change
3481 void CMergeDoc::UpdateResources()
3483 if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3484 m_strDesc[0] = _("Untitled left");
3485 if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3486 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3487 if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3488 m_strDesc[1] = _("Untitled middle");
3489 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3490 UpdateHeaderPath(nBuffer);
3492 GetParentFrame()->UpdateResources();
3493 ForEachView([](auto& pView) { pView->UpdateResources(); });
3496 // Return current word breaking break type setting (whitespace only or include punctuation)
3497 bool CMergeDoc::GetBreakType() const
3499 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3503 // Return true to do line diff colors at the byte level (false to do them at word level)
3504 bool CMergeDoc::GetByteColoringOption() const
3506 // color at byte level if 'break_on_words' option not set
3507 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3511 /// Swap files and update views
3512 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3514 if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3517 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3519 int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3520 int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3521 m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3522 m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3526 // Swap buffers and so on
3527 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3528 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3529 std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3530 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3531 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3532 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3533 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3534 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3536 m_filePaths.Swap(nFromIndex, nToIndex);
3537 m_diffList.Swap(nFromIndex, nToIndex);
3538 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3539 swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3541 ClearWordDiffCache();
3543 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3545 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3546 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3547 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3550 UpdateHeaderPath(nBuffer);
3552 GetParentFrame()->UpdateSplitter();
3553 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3555 UpdateAllViews(nullptr);
3560 * @brief Reloads the opened files
3562 void CMergeDoc::OnFileReload()
3564 if (!PromptAndSaveIfNeeded(true))
3567 FileLocation fileloc[3];
3569 for (int pane = 0; pane < m_nBuffers; pane++)
3571 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3572 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3573 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3574 fileloc[pane].setPath(m_filePaths[pane]);
3576 CPoint pt = GetActiveMergeView()->GetCursorPos();
3577 auto columnWidths = m_ptBuf[0]->GetColumnWidths();
3578 if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3580 m_ptBuf[0]->SetColumnWidths(columnWidths);
3581 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3586 * @brief Display encodings to user
3588 void CMergeDoc::OnFileEncoding()
3590 DoFileEncodingDialog();
3593 void CMergeDoc::OnOpenWithUnpacker()
3595 CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
3596 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3597 CSelectPluginDlg::PluginType::Unpacker, false);
3598 if (dlg.DoModal() != IDOK)
3601 if (!PromptAndSaveIfNeeded(true))
3604 PackingInfo infoUnpacker(dlg.GetPluginPipeline());
3605 PathContext paths = m_filePaths;
3606 DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
3607 String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
3608 int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3609 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3611 if (GetMainFrame()->DoFileOpen(nID, &paths, dwFlags, strDesc, _T(""), &infoUnpacker))
3612 GetParentFrame()->DestroyWindow();
3615 void CMergeDoc::OnApplyPrediffer()
3617 PrediffingInfo prediffer;
3618 GetPrediffer(&prediffer);
3619 // let the user choose a handler
3620 CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
3621 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3622 CSelectPluginDlg::PluginType::Prediffer, false);
3623 if (dlg.DoModal() != IDOK)
3625 prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
3626 SetPrediffer(&prediffer);
3627 m_CurrentPredifferID = -1;
3628 FlushAndRescan(true);
3633 * @brief Create the dynamic submenu for prediffers
3635 * @note The plugins are grouped in (suggested) and (not suggested)
3636 * The IDs follow the order of GetAvailableScripts
3638 * suggested 0 ID_1ST + 0
3639 * suggested 1 ID_1ST + 2
3640 * suggested 2 ID_1ST + 5
3641 * not suggested 0 ID_1ST + 1
3642 * not suggested 1 ID_1ST + 3
3643 * not suggested 2 ID_1ST + 4
3645 HMENU CMergeDoc::createPrediffersSubmenu(HMENU hMenu)
3648 int j = GetMenuItemCount(hMenu);
3650 DeleteMenu(hMenu, 0, MF_BYPOSITION);
3653 AppendMenu(hMenu, MF_STRING, ID_NO_PREDIFFER, _("No prediffer (normal)").c_str());
3655 if (!GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED))
3658 m_CurrentPredifferID = -1;
3660 // compute the m_CurrentPredifferID (to set the radio button)
3661 PrediffingInfo prediffer;
3662 GetPrediffer(&prediffer);
3663 if (prediffer.GetPluginPipeline().empty())
3664 m_CurrentPredifferID = ID_NO_PREDIFFER;
3666 // get the scriptlet files
3667 const auto& [ suggestedPlugins, allPlugins ]= FileTransform::CreatePluginMenuInfos(
3668 m_strBothFilenames, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3670 // build the menu : first part, suggested plugins
3672 AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3673 AppendMenu(hMenu, MF_STRING, ID_SUGGESTED_PLUGINS, _("Suggested plugins").c_str());
3675 for (const auto& [caption, name, id, plugin ] : suggestedPlugins)
3676 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3678 // build the menu : second part, others plugins
3680 AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3681 AppendMenu(hMenu, MF_STRING, ID_NOT_SUGGESTED_PLUGINS, _("Other plugins").c_str());
3683 String lastPluginName;
3684 String errorMessage;
3685 auto result = prediffer.ParsePluginPipeline(errorMessage);
3686 if (result.size() > 0)
3687 lastPluginName = result.back().name;
3689 for (const auto& [processType, pluginAry] : allPlugins)
3691 for (const auto& [caption, name, id, plugin] : pluginAry)
3695 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3696 if (lastPluginName == plugin->m_name)
3697 m_CurrentPredifferID = id;
3706 * @brief Called when an editor script item is updated
3708 void CMergeDoc::OnUpdatePrediffer(CCmdUI* pCmdUI)
3710 pCmdUI->Enable(true);
3712 PrediffingInfo prediffer;
3713 GetPrediffer(&prediffer);
3715 if (prediffer.GetPluginPipeline().find(_T("<Automatic>")) != String::npos)
3717 pCmdUI->SetRadio(false);
3721 // Detect when CDiffWrapper::RunFileDiff has canceled a buggy prediffer
3722 if (prediffer.GetPluginPipeline().empty())
3723 m_CurrentPredifferID = ID_NO_PREDIFFER;
3725 pCmdUI->SetRadio(pCmdUI->m_nID == static_cast<UINT>(m_CurrentPredifferID));
3729 * @brief Handler for all prediffer choices, including ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO, ID_NO_PREDIFFER, & specific prediffers
3731 void CMergeDoc::OnPrediffer(UINT nID )
3733 SetPredifferByMenu(nID);
3734 FlushAndRescan(true);
3739 * @brief Handler for all prediffer choices.
3740 * Prediffer choises include ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO,
3741 * ID_NO_PREDIFFER, & specific prediffers.
3743 void CMergeDoc::SetPredifferByMenu(UINT nID)
3745 // update data for the radio button
3746 m_CurrentPredifferID = nID;
3748 if (nID == ID_NO_PREDIFFER)
3750 // All flags are set correctly during the construction
3751 PrediffingInfo infoPrediffer(false);
3752 SetPrediffer(&infoPrediffer);
3756 String pluginName = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3758 // build a PrediffingInfo structure fom the ID
3759 PrediffingInfo prediffer(pluginName);
3761 // update the prediffer and rescan
3762 SetPrediffer(&prediffer);
3765 void CMergeDoc::OnBnClickedFileEncoding()
3767 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3769 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3770 DoFileEncodingDialog();
3773 void CMergeDoc::OnBnClickedPlugin()
3775 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3777 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3778 OnOpenWithUnpacker();
3781 void CMergeDoc::OnBnClickedHexView()
3783 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3786 void CMergeDoc::OnOK()
3788 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3790 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3793 void CMergeDoc::OnFileRecompareAsText()
3795 m_bEnableTableEditing = false;
3799 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3801 pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
3804 void CMergeDoc::OnFileRecompareAsTable()
3806 m_bEnableTableEditing = true;
3810 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3812 pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3815 void CMergeDoc::OnFileRecompareAs(UINT nID)
3817 if (!PromptAndSaveIfNeeded(true))
3820 DWORD dwFlags[3] = { 0 };
3821 FileLocation fileloc[3];
3823 int nBuffers = m_nBuffers;
3824 CDirDoc *pDirDoc = m_pDirDoc->GetMainView() ? m_pDirDoc :
3825 static_cast<CDirDoc*>(theApp.m_pDirTemplate->CreateNewDocument());
3826 PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
3828 for (int pane = 0; pane < m_nBuffers; pane++)
3830 fileloc[pane].setPath(m_filePaths[pane]);
3831 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3832 strDesc[pane] = m_strDesc[pane];
3834 if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
3836 infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
3837 nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3838 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3841 if (GetMainFrame()->ShowMergeDoc(nID, pDirDoc, nBuffers, fileloc, dwFlags, strDesc, _T(""), &infoUnpacker))
3842 GetParentFrame()->DestroyWindow();
3845 // Return file extension either from file name
3846 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3849 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3854 * @brief Generate report from file compare results.
3856 bool CMergeDoc::GenerateReport(const String& sFileName) const
3858 // calculate HTML font size
3861 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3862 m_pView[0][0]->GetFont(lf);
3863 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3865 // create HTML report
3867 if (!file.Open(sFileName, _T("wt")))
3869 String errMsg = GetSysError(GetLastError());
3870 String msg = strutils::format_string1(
3871 _("Error creating the report:\n%1"), errMsg);
3872 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3876 file.SetCodepage(ucr::CP_UTF_8);
3878 CString headerText =
3879 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3880 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3883 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3884 _T("<title>WinMerge File Compare Report</title>\n")
3885 _T("<style type=\"text/css\">\n")
3887 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3888 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3889 _T("tr { vertical-align: top; }\n")
3890 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3896 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3900 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3901 file.WriteString(header);
3904 // If archive, use archive path + folder + filename inside archive
3905 // If desc text given, use it
3906 PathContext paths = m_filePaths;
3907 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3909 for (int i = 0; i < paths.GetSize(); i++)
3910 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3914 for (int i = 0; i < paths.GetSize(); i++)
3916 if (!m_strDesc[i].empty())
3917 paths[i] = m_strDesc[i];
3923 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3925 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3926 (double)100 / m_nBuffers);
3927 file.WriteString(data);
3928 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3929 file.WriteString(_T("</th>\n"));
3936 // write the body of the report
3938 int nLineCount[3] = {0};
3940 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3941 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3945 file.WriteString(_T("<tr>\n"));
3946 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3948 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3950 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3954 if (idx[nBuffer] < nLineCount[nBuffer])
3957 int iVisibleLineNumber = 0;
3958 String tdtag = _T("<td class=\"ln\">");
3959 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3960 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3962 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3965 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3966 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3969 if (iVisibleLineNumber > 0)
3971 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3972 iVisibleLineNumber = 0;
3975 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3977 if (iVisibleLineNumber > 0)
3978 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3980 tdtag += _T("</td>");
3981 file.WriteString(tdtag);
3983 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3987 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3988 file.WriteString(_T("\n"));
3990 file.WriteString(_T("</tr>\n"));
3992 bool bBorderLine = false;
3993 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3995 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
4001 file.WriteString(_T("<tr height=1>"));
4002 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
4004 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
4005 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
4007 file.WriteString(_T("<td></td><td></td>"));
4009 file.WriteString(_T("</tr>\n"));
4012 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
4027 * @brief Generate report from file compare results.
4029 void CMergeDoc::OnToolsGenerateReport()
4034 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
4037 if (GenerateReport(s.c_str()))
4038 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
4042 * @brief Generate patch from files selected.
4044 * Creates a patch from selected files in active directory compare, or
4045 * active file compare. Files in file compare must be saved before
4048 void CMergeDoc::OnToolsGeneratePatch()
4050 // If there are changes in files, tell user to save them first
4053 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
4058 patcher.AddFiles(m_filePaths.GetLeft(),
4059 m_filePaths.GetRight());
4060 patcher.CreatePatch();
4064 * @brief Add synchronization point
4066 void CMergeDoc::AddSyncPoint()
4069 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4071 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
4072 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
4075 // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
4076 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4077 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
4079 LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
4083 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4084 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
4085 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
4087 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4088 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
4090 m_bHasSyncPoints = true;
4092 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
4094 FlushAndRescan(true);
4098 * @brief Delete a synchronization point
4100 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
4102 const auto syncpoints = GetSyncPointList();
4103 for (auto syncpnt : syncpoints)
4105 if (syncpnt[pane] == nLine)
4107 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4108 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
4112 if (syncpoints.size() == 1)
4113 m_bHasSyncPoints = false;
4116 FlushAndRescan(true);
4121 * @brief Clear Synchronization points
4123 void CMergeDoc::ClearSyncPoints()
4125 if (!m_bHasSyncPoints)
4128 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4130 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
4131 for (int nLine = 0; nLine < nLineCount; ++nLine)
4133 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
4134 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
4138 m_bHasSyncPoints = false;
4140 FlushAndRescan(true);
4143 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
4145 std::vector<std::vector<int> > list;
4146 if (!m_bHasSyncPoints)
4148 int idx[3] = {-1, -1, -1};
4149 std::vector<int> points(m_nBuffers);
4150 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4151 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
4152 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
4154 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
4155 for (int nLine = 0; nLine < nLineCount; ++nLine)
4157 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
4160 if (static_cast<int>(list.size()) <= idx[nBuffer])
4161 list.push_back(points);
4162 list[idx[nBuffer]][nBuffer] = nLine;