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"
31 #include "OptionsDef.h"
32 #include "DiffFileInfo.h"
33 #include "SaveClosingDlg.h"
34 #include "OpenTableDlg.h"
37 #include "OptionsMgr.h"
38 #include "OptionsDiffOptions.h"
39 #include "MergeLineFlags.h"
40 #include "FileOrFolderSelect.h"
41 #include "LineFiltersList.h"
42 #include "SubstitutionFiltersList.h"
44 #include "codepage_detect.h"
45 #include "SelectUnpackerDlg.h"
46 #include "EncodingErrorBar.h"
47 #include "MergeCmdLineInfo.h"
49 #include "Constants.h"
50 #include "Merge7zFormatMergePluginImpl.h"
52 #include "PatchTool.h"
55 #include "stringdiffs.h"
63 int CMergeDoc::m_nBuffersTemp = 2;
65 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
67 /////////////////////////////////////////////////////////////////////////////
70 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
72 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
73 //{{AFX_MSG_MAP(CMergeDoc)
74 ON_COMMAND(ID_FILE_SAVE, OnFileSave)
75 ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
76 ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
77 ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
78 ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
79 ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
80 ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
81 ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
82 ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
83 ON_UPDATE_COMMAND_UI(ID_STATUS_PLUGIN, OnUpdatePluginName)
84 ON_COMMAND(ID_TOOLS_GENERATEREPORT, OnToolsGenerateReport)
85 ON_COMMAND(ID_TOOLS_GENERATEPATCH, OnToolsGeneratePatch)
86 ON_COMMAND(ID_RESCAN, OnFileReload)
87 ON_COMMAND(ID_FILE_ENCODING, OnFileEncoding)
88 ON_COMMAND_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnDiffContext)
89 ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_DIFFCONTEXT_ALL, ID_VIEW_DIFFCONTEXT_INVERT, OnUpdateDiffContext)
90 ON_COMMAND(ID_POPUP_OPEN_WITH_UNPACKER, OnCtxtOpenWithUnpacker)
91 ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
92 ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
93 ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
94 ON_COMMAND(IDOK, OnOK)
95 ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
96 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
97 ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
98 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
99 ON_COMMAND(ID_MERGE_COMPARE_XML, OnFileRecompareAsXML)
100 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_XML, OnUpdateFileRecompareAsXML)
101 ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
102 ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
106 /////////////////////////////////////////////////////////////////////////////
107 // CMergeDoc construction/destruction
110 * @brief Constructor.
112 CMergeDoc::CMergeDoc()
113 : m_bEnableRescan(true)
115 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
118 , m_pInfoUnpacker(new PackingInfo)
119 , m_pEncodingErrorBar(nullptr)
120 , m_bHasSyncPoints(false)
121 , m_bAutoMerged(false)
124 , m_bAutomaticRescan(false)
126 DIFFOPTIONS options = {0};
128 m_nBuffers = m_nBuffersTemp;
129 m_filePaths.SetSize(m_nBuffers);
131 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
133 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
134 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
135 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
136 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
137 m_bEditAfterRescan[nBuffer] = false;
140 m_bEnableRescan = true;
141 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
143 // COleDateTime m_LastRescan
144 curUndo = undoTgt.begin();
145 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
146 m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
148 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
149 Options::DiffOptions::Load(GetOptionsMgr(), options);
151 m_diffWrapper.SetOptions(&options);
152 m_diffWrapper.SetPrediffer(nullptr);
158 * Informs associated dirdoc that mergedoc is closing.
160 CMergeDoc::~CMergeDoc()
162 if (m_pDirDoc != nullptr)
164 m_pDirDoc->MergeDocClosing(this);
170 * @brief Deleted data associated with doc before closing.
172 void CMergeDoc::DeleteContents ()
174 CDocument::DeleteContents ();
175 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
177 m_ptBuf[nBuffer]->FreeAll ();
178 m_tempFiles[nBuffer].Delete();
183 * @brief Called when new document is created.
185 * Initialises buffers.
187 BOOL CMergeDoc::OnNewDocument()
189 if (!CDocument::OnNewDocument())
192 SetTitle(_("File Comparison").c_str());
194 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
195 m_ptBuf[nBuffer]->InitNew ();
200 * @brief Return active merge edit view (or left one if neither active)
202 CMergeEditView * CMergeDoc::GetActiveMergeView()
204 CView * pActiveView = GetParentFrame()->GetActiveView();
205 CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
206 if (pMergeEditView == nullptr)
207 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
208 return pMergeEditView;
211 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
213 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
216 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
218 if (infoNewHandler != nullptr)
220 *m_pInfoUnpacker = *infoNewHandler;
224 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
226 m_diffWrapper.SetPrediffer(infoPrediffer);
228 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
230 m_diffWrapper.GetPrediffer(infoPrediffer);
233 /////////////////////////////////////////////////////////////////////////////
234 // CMergeDoc serialization
236 void CMergeDoc::Serialize(CArchive& ar)
238 ASSERT(false); // we do not use CDocument serialization
241 /////////////////////////////////////////////////////////////////////////////
242 // CMergeDoc commands
245 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
248 * original file is Ansi :
249 * buffer -> save as Ansi -> Ansi plugins -> diffutils
250 * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
251 * buffer -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
252 * (the plugins are optional, not the conversion)
253 * @todo Show SaveToFile() errors?
255 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
257 // and we don't repack the file
258 PackingInfo * tempPacker = nullptr;
260 // write buffer out to temporary file
262 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
263 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
267 * @brief Save files to temp files & compare again.
269 * @param bBinary [in,out] [in] If true, compare two binary files
270 * [out] If true binary file was detected.
271 * @param bIdentical [out] If true files were identical
272 * @param bForced [in] If true, suppressing is ignored and rescan
274 * @return Tells if rescan was successfully, was suppressed, or
276 * If this code is OK, Rescan has detached the views temporarily
277 * (positions of cursors have been lost)
278 * @note Rescan() ALWAYS compares temp files. Actual user files are not
279 * touched by Rescan().
280 * @sa CDiffWrapper::RunFileDiff()
282 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
283 bool bForced /* =false */)
285 DIFFOPTIONS diffOptions = {0};
286 DiffFileInfo fileInfo;
287 bool diffSuccess = false;
288 int nResult = RESCAN_OK;
289 FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
294 if (!m_bEnableRescan)
295 return RESCAN_SUPPRESSED;
298 ClearWordDiffCache();
300 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
302 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
306 m_diffWrapper.SetFilterList(_T(""));
309 if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
311 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
315 m_diffWrapper.SetSubstitutionList(nullptr);
318 if (GetView(0, 0)->m_CurSourceDef->type != 0)
319 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
321 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
323 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
325 // Check if files have been modified since last rescan
326 // Ignore checking in case of scratchpads (empty filenames)
327 if (!m_filePaths[nBuffer].empty())
329 Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
330 fileInfo, false, nBuffer);
333 m_LastRescan = COleDateTime::GetCurrentTime();
335 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
336 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
338 if (Changed[nBuffer] == FileChange::Removed)
340 String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
341 ShowMessageBox(msg, MB_ICONWARNING);
342 bool bSaveResult = false;
343 bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
344 if (!ok || !bSaveResult)
346 return RESCAN_FILE_ERR;
350 String temp = m_tempFiles[nBuffer].GetPath();
352 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
354 return RESCAN_TEMP_ERR;
359 String tempPath = env::GetTemporaryPath();
361 // Set up DiffWrapper
362 m_diffWrapper.GetOptions(&diffOptions);
367 m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
368 // Clear moved lines lists
369 if (m_diffWrapper.GetDetectMovedBlocks())
371 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
372 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
375 // Set paths for diffing and run diff
376 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
378 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
380 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
381 m_diffWrapper.SetCompareFiles(m_filePaths);
385 if (!HasSyncPoints())
387 // Save text buffer to file
388 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
390 m_ptBuf[nBuffer]->SetTempPath(tempPath);
391 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
394 m_diffWrapper.SetCreateDiffList(&m_diffList);
395 diffSuccess = m_diffWrapper.RunFileDiff();
398 m_diffWrapper.GetDiffStatus(&status);
399 if (bBinary) // believe caller if we were told these are binaries
400 status.bBinaries = true;
404 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();
405 int nStartLine[3] = {0};
406 int nLines[3], nRealLine[3];
407 for (size_t i = 0; i <= syncpoints.size(); ++i)
409 // Save text buffer to file
410 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
412 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
413 m_ptBuf[nBuffer]->SetTempPath(tempPath);
414 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(),
415 nStartLine[nBuffer], nLines[nBuffer]);
419 m_diffWrapper.SetCreateDiffList(&templist);
420 diffSuccess = m_diffWrapper.RunFileDiff();
421 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
422 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
424 // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
425 if (i == 0 && templist.GetSize() > 0)
426 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
427 if (nStartLine[nBuffer] == 0)
429 bool isEmptyFile = true;
430 for (int j = 0; j < nLines[nBuffer]; j++)
432 if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
441 templist.GetDiff(0, di);
442 if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
444 di.end[nBuffer] = -1;
445 templist.SetDiff(0, di);
450 m_diffList.AppendDiffList(templist, nRealLine);
451 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
452 nStartLine[nBuffer] += nLines[nBuffer];
455 DIFFSTATUS status_part;
456 m_diffWrapper.GetDiffStatus(&status_part);
457 if (bBinary) // believe caller if we were told these are binaries
458 status.bBinaries = true;
459 status.MergeStatus(status_part);
461 m_diffWrapper.SetCreateDiffList(&m_diffList);
464 // If one file has EOL before EOF and other not...
465 if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
467 // ..last DIFFRANGE of file which has EOL must be
468 // fixed to contain last line too
469 int lineCount[3] = { 0,0,0 };
470 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
471 lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
472 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
475 // set identical/diff result as recorded by diffutils
476 identical = status.Identical;
478 // Determine errors and binary file compares
480 nResult = RESCAN_FILE_ERR;
481 else if (status.bBinaries)
487 // Now update views and buffers for ghost lines
489 // Prevent displaying views during this update
490 // BTW, this solves the problem of double asserts
491 // (during the display of an assert message box, a second assert in one of the
492 // display functions happens, and hides the first assert)
493 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
495 // Remove blank lines and clear winmerge flags
496 // this operation does not change the modified flag
497 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
498 m_ptBuf[nBuffer]->prepareForRescan();
500 // Divide diff blocks to match lines.
501 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
504 // Analyse diff-list (updating real line-numbers)
505 // this operation does not change the modified flag
508 // Hide identical lines if diff-context is not 'All'
511 // Apply flags to lines that are trivial
512 PrediffingInfo infoPrediffer;
513 GetPrediffer(&infoPrediffer);
514 if (!infoPrediffer.m_PluginName.empty())
517 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
518 if (m_diffWrapper.GetDetectMovedBlocks())
521 // After PrimeTextBuffers() we know amount of real diffs
522 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
524 // Identical files are also updated
525 if (!m_diffList.HasSignificantDiffs())
526 identical = IDENTLEVEL::ALL;
528 ForEachView([](auto& pView) {
529 // just apply some options to the views
530 pView->PrimeListWithFile();
531 // Now buffers data are valid
532 pView->ReAttachToBuffer();
534 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
536 m_bEditAfterRescan[nBuffer] = false;
540 if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
541 identical == IDENTLEVEL::ALL &&
542 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
543 [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
544 identical = IDENTLEVEL::NONE;
546 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
551 void CMergeDoc::CheckFileChanged(void)
554 DiffFileInfo fileInfo;
555 FileChange FileChange[3];
557 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
559 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
562 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
565 bool bDoReload = false;
566 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
568 if (FileChange[nBuffer] == FileChange::Changed)
570 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]);
571 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
578 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
580 if (FileChange[nBuffer] == FileChange::Changed)
582 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
583 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
589 /** @brief Apply flags to lines that are trivial */
590 void CMergeDoc::FlagTrivialLines(void)
592 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
594 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
597 for (int file = 0; file < m_nBuffers; ++file)
599 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
600 str[file] = p ? p : _T("");
603 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
605 DIFFOPTIONS diffOptions = {0};
606 m_diffWrapper.GetOptions(&diffOptions);
608 // Make the call to stringdiffs, which does all the hard & tedious computations
609 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
610 !diffOptions.bIgnoreCase,
611 !diffOptions.bIgnoreEol,
612 diffOptions.nIgnoreWhitespace,
613 GetBreakType(), // whitespace only or include punctuation
614 GetByteColoringOption());
615 if (!worddiffs.empty())
617 for (int file = 0; file < m_nBuffers; ++file)
618 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
625 /** @brief Adjust all different lines that were detected as actually matching moved lines */
626 void CMergeDoc::FlagMovedLines(void)
629 MovedLines *pMovedLines;
631 pMovedLines = m_diffWrapper.GetMovedLines(0);
632 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
634 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
637 TRACE(_T("%d->%d\n"), i, j);
639 // We only flag lines that are already marked as being different
640 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
641 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
643 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
644 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
646 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
647 if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
648 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
654 pMovedLines = m_diffWrapper.GetMovedLines(1);
655 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
657 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
660 TRACE(_T("%d->%d\n"), i, j);
662 // We only flag lines that are already marked as being different
663 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
664 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
666 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
667 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
669 int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
670 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
671 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
680 pMovedLines = m_diffWrapper.GetMovedLines(1);
681 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
683 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
686 TRACE(_T("%d->%d\n"), i, j);
688 // We only flag lines that are already marked as being different
689 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
690 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
692 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
693 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
695 int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
696 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
697 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
703 pMovedLines = m_diffWrapper.GetMovedLines(2);
704 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
706 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
709 TRACE(_T("%d->%d\n"), i, j);
711 // We only flag lines that are already marked as being different
712 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
713 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
715 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
716 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
718 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
719 if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
720 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
726 // todo: Need to record actual moved information
729 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
731 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
733 GetParentFrame()->InitialUpdateFrame(this, true);
734 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
736 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
740 * @brief Prints (error) message by rescan status.
742 * @param nRescanResult [in] Resultcocode from rescan().
743 * @param bIdentical [in] Were files identical?.
744 * @sa CMergeDoc::Rescan()
746 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
748 // Rescan was suppressed, there is no sensible status
749 if (nRescanResult == RESCAN_SUPPRESSED)
754 if (nRescanResult == RESCAN_FILE_ERR)
756 s = _("An error occurred while comparing the files.");
758 ShowMessageBox(s, MB_ICONSTOP);
762 if (nRescanResult == RESCAN_TEMP_ERR)
764 s = _("Temporary files could not be created. Check your temporary path settings.");
766 ShowMessageBox(s, MB_ICONSTOP);
770 // Files are not binaries, but they are identical
771 if (identical != IDENTLEVEL::NONE)
773 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
774 [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
778 bool CMergeDoc::Undo()
784 * @brief An instance of RescanSuppress prevents rescan during its lifetime
785 * (or until its Clear method is called, which ends its effect).
790 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
793 m_bPrev = doc.m_bEnableRescan;
794 m_doc.m_bEnableRescan = false;
801 m_doc.m_bEnableRescan = m_bPrev;
815 * @brief Copy all diffs from one side to side.
816 * @param [in] srcPane Source side from which diff is copied
817 * @param [in] dstPane Destination side
819 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
821 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
825 * @brief Copy range of diffs from one side to side.
826 * This function copies given range of differences from side to another.
827 * Ignored differences are skipped, and not copied.
828 * @param [in] srcPane Source side from which diff is copied
829 * @param [in] dstPane Destination side
830 * @param [in] firstDiff First diff copied (0-based index)
831 * @param [in] lastDiff Last diff copied (0-based index)
833 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
836 if (firstDiff > lastDiff)
837 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
839 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
840 if (lastDiff > m_diffList.GetSize() - 1)
841 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
844 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
845 firstDiff = max(0, firstDiff);
846 if (firstDiff > lastDiff)
849 RescanSuppress suppressRescan(*this);
851 // Note we don't care about m_nDiffs count to become zero,
852 // because we don't rescan() so it does not change
854 SetCurrentDiff(lastDiff);
856 bool bGroupWithPrevious = false;
857 if (firstWordDiff <= 0 && lastWordDiff == -1)
859 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
860 return; // sync failure
864 if (!WordListCopy(srcPane, dstPane, lastDiff,
865 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
866 return; // sync failure
869 SetEditedAfterRescan(dstPane);
871 int nGroup = GetActiveMergeView()->m_nThisGroup;
872 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
873 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
874 CPoint currentPosSrc = pViewSrc->GetCursorPos();
876 CPoint currentPosDst = pViewDst->GetCursorPos();
880 pViewDst->SetCursorPos(pt);
881 pViewDst->SetNewSelection(pt, pt, false);
882 pViewDst->SetNewAnchor(pt);
884 // copy from bottom up is more efficient
885 for (int i = lastDiff - 1; i >= firstDiff; --i)
887 if (m_diffList.IsDiffSignificant(i))
890 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
891 if (currentPosDst.y > pdi->dend)
893 if (pdi->blank[dstPane] >= 0)
894 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
895 else if (pdi->blank[srcPane] >= 0)
896 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
898 // Group merge with previous (merge undo data to one action)
899 bGroupWithPrevious = true;
900 if (i > firstDiff || firstWordDiff <= 0)
902 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
903 break; // sync failure
907 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
908 break; // sync failure
913 ForEachView(dstPane, [currentPosDst](auto& pView) {
914 pView->SetCursorPos(currentPosDst);
915 pView->SetNewSelection(currentPosDst, currentPosDst, false);
916 pView->SetNewAnchor(currentPosDst);
919 suppressRescan.Clear(); // done suppress Rescan
923 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
924 int firstLineDiff, int lastLineDiff)
926 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
927 firstDiff = max(0, firstDiff);
928 if (firstDiff > lastDiff)
931 RescanSuppress suppressRescan(*this);
933 bool bGroupWithPrevious = false;
934 if (firstLineDiff <= 0 && lastLineDiff == -1)
936 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
937 return; // sync failure
941 if (!PartialListCopy(srcPane, dstPane, lastDiff,
942 (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
943 return; // sync failure
947 SetEditedAfterRescan(dstPane);
949 int nGroup = GetActiveMergeView()->m_nThisGroup;
950 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
951 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
952 CPoint currentPosSrc = pViewSrc->GetCursorPos();
954 CPoint currentPosDst = pViewDst->GetCursorPos();
958 pViewDst->SetCursorPos(pt);
959 pViewDst->SetNewSelection(pt, pt, false);
960 pViewDst->SetNewAnchor(pt);
962 // copy from bottom up is more efficient
963 for (int i = lastDiff - 1; i >= firstDiff; --i)
965 if (m_diffList.IsDiffSignificant(i))
968 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
969 if (currentPosDst.y > pdi->dend)
971 if (pdi->blank[dstPane] >= 0)
972 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
973 else if (pdi->blank[srcPane] >= 0)
974 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
976 // Group merge with previous (merge undo data to one action)
977 bGroupWithPrevious = true;
978 if (i > firstDiff || firstLineDiff <= 0)
980 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
981 break; // sync failure
985 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
986 break; // sync failure
991 ForEachView(dstPane, [currentPosDst](auto& pView) {
992 pView->SetCursorPos(currentPosDst);
993 pView->SetNewSelection(currentPosDst, currentPosDst, false);
994 pView->SetNewAnchor(currentPosDst);
997 suppressRescan.Clear(); // done suppress Rescan
1001 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1003 template<class Type>
1004 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1006 bool equal_all = middle == left && middle == right && left == right;
1008 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1009 bool conflict = middle != left && middle != right && left != right;
1011 return std::pair<MergeResult, Type>(Conflict, left);
1016 return std::pair<MergeResult, Type>(Merged, middle);
1020 return std::pair<MergeResult, Type>(Merged, right);
1022 return std::pair<MergeResult, Type>(Merged, left);
1026 return std::pair<MergeResult, Type>(Merged, middle);
1029 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1033 * @brief Do auto-merge.
1034 * @param [in] dstPane Destination side
1036 void CMergeDoc::DoAutoMerge(int dstPane)
1038 const int lastDiff = m_diffList.GetSize() - 1;
1039 const int firstDiff = 0;
1040 bool bGroupWithPrevious = false;
1041 int autoMergedCount = 0;
1042 int unresolvedConflictCount = 0;
1044 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
1045 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1046 if (mergedEncoding.first == Merged)
1048 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1049 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1051 else if (mergedEncoding.first == Conflict)
1052 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1054 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
1055 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1056 if (mergedEOLStyle.first == Merged)
1058 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1059 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1061 else if (mergedEOLStyle.first == Conflict)
1062 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1064 RescanSuppress suppressRescan(*this);
1066 // Note we don't care about m_nDiffs count to become zero,
1067 // because we don't rescan() so it does not change
1069 SetCurrentDiff(lastDiff);
1071 SetEditedAfterRescan(dstPane);
1073 int nGroup = GetActiveMergeView()->m_nThisGroup;
1074 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1075 CPoint currentPosDst = pViewDst->GetCursorPos();
1076 currentPosDst.x = 0;
1079 pViewDst->SetCursorPos(pt);
1080 pViewDst->SetNewSelection(pt, pt, false);
1081 pViewDst->SetNewAnchor(pt);
1083 // copy from bottom up is more efficient
1084 for (int i = lastDiff; i >= firstDiff; --i)
1086 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1087 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1091 if (currentPosDst.y > pdi->dend)
1093 if (pdi->blank[dstPane] >= 0)
1094 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1095 else if (pdi->blank[srcPane] >= 0)
1096 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1098 // Group merge with previous (merge undo data to one action)
1099 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1100 break; // sync failure
1101 if (!bGroupWithPrevious)
1102 bGroupWithPrevious = true;
1105 if (pdi->op == OP_DIFF)
1106 ++unresolvedConflictCount;
1109 ForEachView(dstPane, [currentPosDst](auto& pView) {
1110 pView->SetCursorPos(currentPosDst);
1111 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1112 pView->SetNewAnchor(currentPosDst);
1115 suppressRescan.Clear(); // done suppress Rescan
1117 UpdateHeaderPath(dstPane);
1119 if (autoMergedCount > 0)
1120 m_bAutoMerged = true;
1122 // move to first conflict
1123 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1125 pViewDst->SelectDiff(nDiff, true, false);
1128 strutils::format_string2(
1129 _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1130 strutils::format(_T("%d"), autoMergedCount),
1131 strutils::format(_T("%d"), unresolvedConflictCount)),
1132 MB_ICONINFORMATION);
1136 * @brief Sanity check difference.
1138 * Checks that lines in difference are inside difference in both files.
1139 * If file is edited, lines added or removed diff lines get out of sync and
1140 * merging fails miserably.
1142 * @param [in] dr Difference to check.
1143 * @return true if difference lines match, false otherwise.
1145 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1147 const int cd_dbegin = dr.dbegin;
1148 const int cd_dend = dr.dend;
1150 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1152 // Must ensure line number is in range before getting line flags
1153 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1156 // Optimization - check last line first so we don't need to
1157 // check whole diff for obvious cases
1158 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1159 if (!(dwFlags & LF_WINMERGE_FLAGS))
1163 for (int line = cd_dbegin; line < cd_dend; line++)
1165 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1167 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1168 if (!(dwFlags & LF_WINMERGE_FLAGS))
1176 * @brief Copy selected (=current) difference from from side to side.
1177 * @param [in] srcPane Source side from which diff is copied
1178 * @param [in] dstPane Destination side
1179 * @param [in] nDiff Diff to copy, if -1 function determines it.
1180 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1181 * @return true if ok, false if sync failure & need to abort copy
1182 * previous action (allows one undo for copy all)
1184 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1185 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1187 int nGroup = GetActiveMergeView()->m_nThisGroup;
1188 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1189 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1190 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1192 // suppress Rescan during this method
1193 // (Not only do we not want to rescan a lot of times, but
1194 // it will wreck the line status array to rescan as we merge)
1195 RescanSuppress suppressRescan(*this);
1197 // If diff-number not given, determine it from active view
1200 nDiff = GetCurrentDiff();
1202 // No current diff, but maybe cursor is in diff?
1203 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1204 pViewDst->IsCursorInDiff()))
1206 // Find out diff under cursor
1207 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1208 nDiff = m_diffList.LineToDiff(ptCursor.y);
1215 VERIFY(m_diffList.GetDiff(nDiff, cd));
1216 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1217 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1218 bool bSrcWasMod = sbuf.IsModified();
1219 const int cd_dbegin = cd.dbegin;
1220 const int cd_dend = cd.dend;
1221 const int cd_blank = cd.blank[srcPane];
1222 bool bInSync = SanityCheckDiff(cd);
1226 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1227 return false; // abort copying
1230 // If we remove whole diff from current view, we must fix cursor
1231 // position first. Normally we would move to end of previous line,
1232 // but we want to move to begin of that line for usability.
1235 CPoint currentPos = pViewDst->GetCursorPos();
1237 if (currentPos.y > cd_dend)
1239 if (cd.blank[dstPane] >= 0)
1240 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1241 else if (cd.blank[srcPane] >= 0)
1242 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1244 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1247 // if the current diff contains missing lines, remove them from both sides
1248 int limit = cd_dend;
1250 // curView is the view which is changed, so the opposite of the source view
1251 dbuf.BeginUndoGroup(bGroupWithPrevious);
1254 // text was missing, so delete rest of lines on both sides
1255 // delete only on destination side since rescan will clear the other side
1256 if (cd_dend + 1 < dbuf.GetLineCount())
1258 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1262 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1263 ASSERT(cd_blank > 0);
1264 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1268 dbuf.FlushUndoGroup(pSource);
1269 dbuf.BeginUndoGroup(true);
1273 // copy the selected text over
1274 if (cd_dbegin <= limit)
1276 // text exists on left side, so just replace
1277 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1278 dbuf.FlushUndoGroup(pSource);
1279 dbuf.BeginUndoGroup(true);
1281 dbuf.FlushUndoGroup(pSource);
1286 // reset the mod status of the source view because we do make some
1287 // changes, but none that concern the source text
1288 sbuf.SetModified(bSrcWasMod);
1291 suppressRescan.Clear(); // done suppress Rescan
1296 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1297 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1299 int nGroup = GetActiveMergeView()->m_nThisGroup;
1300 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1301 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1302 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1304 // suppress Rescan during this method
1305 // (Not only do we not want to rescan a lot of times, but
1306 // it will wreck the line status array to rescan as we merge)
1307 RescanSuppress suppressRescan(*this);
1310 VERIFY(m_diffList.GetDiff(nDiff, cd));
1311 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1312 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1313 bool bSrcWasMod = sbuf.IsModified();
1314 const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1315 const int cd_dend = cd.dend;
1316 const int cd_blank = cd.blank[srcPane];
1317 bool bInSync = SanityCheckDiff(cd);
1321 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1322 return false; // abort copying
1325 // If we remove whole diff from current view, we must fix cursor
1326 // position first. Normally we would move to end of previous line,
1327 // but we want to move to begin of that line for usability.
1330 CPoint currentPos = pViewDst->GetCursorPos();
1332 if (currentPos.y > cd_dend)
1334 if (cd.blank[dstPane] >= 0)
1335 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1336 else if (cd.blank[srcPane] >= 0)
1337 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1339 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1342 // if the current diff contains missing lines, remove them from both sides
1343 int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1345 // curView is the view which is changed, so the opposite of the source view
1346 dbuf.BeginUndoGroup(bGroupWithPrevious);
1347 if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1349 // text was missing, so delete rest of lines on both sides
1350 // delete only on destination side since rescan will clear the other side
1351 if (limit+1 < dbuf.GetLineCount())
1353 dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1357 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1358 ASSERT(cd_dbegin > 0);
1359 dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1362 limit = cd_dbegin-1;
1363 dbuf.FlushUndoGroup(pSource);
1364 dbuf.BeginUndoGroup(true);
1367 // copy the selected text over
1368 if (cd_dbegin <= limit)
1370 // text exists on left side, so just replace
1371 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1372 dbuf.FlushUndoGroup(pSource);
1373 dbuf.BeginUndoGroup(true);
1375 dbuf.FlushUndoGroup(pSource);
1380 // reset the mod status of the source view because we do make some
1381 // changes, but none that concern the source text
1382 sbuf.SetModified(bSrcWasMod);
1384 suppressRescan.Clear(); // done suppress Rescan
1389 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1390 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1392 int nGroup = GetActiveMergeView()->m_nThisGroup;
1393 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1394 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1395 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1397 // suppress Rescan during this method
1398 // (Not only do we not want to rescan a lot of times, but
1399 // it will wreck the line status array to rescan as we merge)
1400 RescanSuppress suppressRescan(*this);
1403 VERIFY(m_diffList.GetDiff(nDiff, cd));
1404 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1405 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1406 bool bSrcWasMod = sbuf.IsModified();
1407 const int cd_dbegin = cd.dbegin;
1408 const int cd_dend = cd.dend;
1409 const int cd_blank = cd.blank[srcPane];
1410 bool bInSync = SanityCheckDiff(cd);
1414 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1415 return false; // abort copying
1418 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1420 if (worddiffs.empty())
1423 if (cd.end[srcPane] < cd.begin[srcPane])
1424 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1426 if (firstWordDiff == -1)
1428 if (lastWordDiff == -1)
1429 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1431 // If we remove whole diff from current view, we must fix cursor
1432 // position first. Normally we would move to end of previous line,
1433 // but we want to move to begin of that line for usability.
1436 CPoint currentPos = pViewDst->GetCursorPos();
1438 if (currentPos.y > cd_dend)
1440 if (cd.blank[dstPane] >= 0)
1441 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1442 else if (cd.blank[srcPane] >= 0)
1443 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1445 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1448 // if the current diff contains missing lines, remove them from both sides
1449 int limit = cd_dend;
1451 // curView is the view which is changed, so the opposite of the source view
1452 dbuf.BeginUndoGroup(bGroupWithPrevious);
1454 CString srcText, dstText;
1455 CPoint ptDstStart, ptDstEnd;
1456 CPoint ptSrcStart, ptSrcEnd;
1458 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1459 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1460 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1461 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1462 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1463 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1464 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1465 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1467 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1468 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1470 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1471 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1474 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1475 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1477 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1478 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1480 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1482 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1484 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1485 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1486 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1487 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1488 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1489 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1490 + dstText.Mid(dstEnd - ptDstStart.x);
1493 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1496 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1498 dbuf.FlushUndoGroup(pSource);
1500 // reset the mod status of the source view because we do make some
1501 // changes, but none that concern the source text
1502 sbuf.SetModified(bSrcWasMod);
1504 suppressRescan.Clear(); // done suppress Rescan
1511 * @brief Save file with new filename.
1513 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1514 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1515 * normal filename fails, to let user choose another filename/location.
1516 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1517 * using this function.
1518 * @param [in, out] strPath
1519 * - [in] : Initial path shown to user
1520 * - [out] : Path to new filename if saving succeeds
1521 * @param [in, out] nSaveResult
1522 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1524 * - [out] : Statuscode of this saving try
1525 * @param [in, out] sError Error string from lower level saving code
1526 * @param [in] nBuffer Buffer we are saving
1527 * @return false as long as the user is not satisfied. Calling function
1528 * should not continue until true is returned.
1529 * @sa CMergeDoc::DoSave()
1530 * @sa CMergeDoc::DoSaveAs()
1531 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1533 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1534 int nBuffer, PackingInfo * pInfoTempUnpacker)
1538 String strSavePath; // New path for next saving try
1541 int answer = IDOK; // Set default we use for scratchpads
1543 // We shouldn't get here if saving is succeed before
1544 ASSERT(nSaveResult != SAVE_DONE);
1546 // Select message based on reason function called
1547 if (nSaveResult == SAVE_PACK_FAILED)
1549 if (m_nBuffers == 3)
1551 str = strutils::format_string2(
1553 _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")
1555 _("Plugin '%2' cannot pack your changes to the middle file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"):
1556 _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")),
1557 strPath, pInfoTempUnpacker->m_PluginName);
1561 str = strutils::format_string2(nBuffer == 0 ? _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?") :
1562 _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"),
1563 strPath, pInfoTempUnpacker->m_PluginName);
1565 // replace the unpacker with a "do nothing" unpacker
1566 pInfoTempUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
1570 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);
1573 // SAVE_NO_FILENAME is temporarily used for scratchpad.
1574 // So don't ask about saving in that case.
1575 if (nSaveResult != SAVE_NO_FILENAME)
1576 answer = ShowMessageBox(str, MB_OKCANCEL | MB_ICONWARNING);
1582 title = _("Save Left File As");
1583 else if (nBuffer == m_nBuffers - 1)
1584 title = _("Save Right File As");
1586 title = _("Save Middle File As");
1588 if (SelectFile(GetActiveMergeView()->GetSafeHwnd(), s, false, strPath.c_str(), title))
1590 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1592 nSaveResult = pBuffer->SaveToFile(strSavePath, false, sError,
1595 if (nSaveResult == SAVE_DONE)
1597 // We are saving scratchpad (unnamed file)
1598 if (strPath.empty())
1600 m_nBufferType[nBuffer] = BUFFERTYPE::UNNAMED_SAVED;
1601 m_strDesc[nBuffer].erase();
1604 strPath = strSavePath;
1605 UpdateHeaderPath(nBuffer);
1611 nSaveResult = SAVE_CANCELLED;
1615 nSaveResult = SAVE_CANCELLED;
1622 * @brief Save file creating backups etc.
1624 * Safe top-level file saving function. Checks validity of given path.
1625 * Creates backup file if wanted to. And if saving to given path fails,
1626 * allows user to select new location/name for file.
1627 * @param [in] szPath Path where to save including filename. Can be
1628 * empty/`nullptr` if new file is created (scratchpad) without filename.
1629 * @param [out] bSaveSuccess Will contain information about save success with
1630 * the original name (to determine if file statuses should be changed)
1631 * @param [in] nBuffer Index (0-based) of buffer to save
1632 * @return Tells if caller can continue (no errors happened)
1633 * @note Return value does not tell if SAVING succeeded. Caller must
1634 * Check value of bSaveSuccess parameter after calling this function!
1635 * @note If CMainFrame::m_strSaveAsPath is non-empty, file is saved
1636 * to directory it points to. If m_strSaveAsPath contains filename,
1637 * that filename is used.
1638 * @sa CMergeDoc::TrySaveAs()
1639 * @sa CMainFrame::CheckSavePath()
1640 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1642 bool CMergeDoc::DoSave(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1644 DiffFileInfo fileInfo;
1645 String strSavePath(szPath);
1646 FileChange fileChanged;
1647 bool bApplyToAll = false;
1650 fileChanged = IsFileChangedOnDisk(szPath, fileInfo, true, nBuffer);
1651 if (fileChanged == FileChange::Changed)
1653 String msg = strutils::format_string1(_("Another application has updated file\n%1\nsince WinMerge loaded it.\n\nOverwrite changed file?"), szPath);
1654 if (ShowMessageBox(msg, MB_ICONWARNING | MB_YESNO) == IDNO)
1656 bSaveSuccess = true;
1661 // use a temp packer
1662 // first copy the m_pInfoUnpacker
1663 // if an error arises during packing, change and take a "do nothing" packer
1664 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1666 bSaveSuccess = false;
1668 // Check third arg possibly given from command-line
1669 if (!theApp.m_strSaveAsPath.empty())
1671 if (paths::DoesPathExist(theApp.m_strSaveAsPath) == paths::IS_EXISTING_DIR)
1673 // third arg was a directory, so get append the filename
1675 paths::SplitFilename(szPath, 0, &sname, 0);
1676 strSavePath = theApp.m_strSaveAsPath;
1677 strSavePath = paths::ConcatPath(strSavePath, sname);
1680 strSavePath = theApp.m_strSaveAsPath;
1683 nRetVal = theApp.HandleReadonlySave(strSavePath, false, bApplyToAll);
1684 if (nRetVal == IDCANCEL)
1687 if (!theApp.CreateBackup(false, strSavePath))
1690 // false as long as the user is not satisfied
1691 // true if saving succeeds, even with another filename, or if the user cancels
1692 bool result = false;
1693 // the error code from the latest save operation,
1694 // or SAVE_DONE when the save succeeds
1695 // TODO: Shall we return this code in addition to bSaveSuccess ?
1696 int nSaveErrorCode = SAVE_DONE;
1697 CDiffTextBuffer *pBuffer = m_ptBuf[nBuffer].get();
1699 // Assume empty filename means Scratchpad (unnamed file)
1700 // Todo: This is not needed? - buffer type check should be enough
1701 if (strSavePath.empty())
1702 nSaveErrorCode = SAVE_NO_FILENAME;
1704 // Handle unnamed buffers
1705 if (m_nBufferType[nBuffer] == BUFFERTYPE::UNNAMED)
1706 nSaveErrorCode = SAVE_NO_FILENAME;
1709 if (nSaveErrorCode == SAVE_DONE)
1710 // We have a filename, just try to save
1711 nSaveErrorCode = pBuffer->SaveToFile(strSavePath, false, sError, &infoTempUnpacker);
1713 if (nSaveErrorCode != SAVE_DONE)
1715 // Saving failed, user may save to another location if wants to
1717 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1721 // Saving succeeded with given/selected filename
1722 if (nSaveErrorCode == SAVE_DONE)
1724 // Preserve file times if user wants to
1725 if (GetOptionsMgr()->GetBool(OPT_PRESERVE_FILETIMES))
1727 fileInfo.SetFile(strSavePath);
1730 TFile file(strSavePath);
1731 file.setLastModified(fileInfo.mtime);
1738 m_ptBuf[nBuffer]->SetModified(false);
1739 m_pSaveFileInfo[nBuffer]->Update(strSavePath.c_str());
1740 m_filePaths[nBuffer] = strSavePath;
1741 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer].c_str());
1742 UpdateHeaderPath(nBuffer);
1743 bSaveSuccess = true;
1746 else if (nSaveErrorCode == SAVE_CANCELLED)
1748 // User cancelled current operation, lets do what user wanted to do
1755 * @brief Save file with different filename.
1757 * Safe top-level file saving function. Asks user to select filename
1758 * and path. Does not create backups.
1759 * @param [in] szPath Path where to save including filename. Can be
1760 * empty/`nullptr` if new file is created (scratchpad) without filename.
1761 * @param [out] bSaveSuccess Will contain information about save success with
1762 * the original name (to determine if file statuses should be changed)
1763 * @param [in] nBuffer Index (0-based) of buffer to save
1764 * @return Tells if caller can continue (no errors happened)
1765 * @note Return value does not tell if SAVING succeeded. Caller must
1766 * Check value of bSaveSuccess parameter after calling this function!
1767 * @sa CMergeDoc::TrySaveAs()
1768 * @sa CMainFrame::CheckSavePath()
1769 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1771 bool CMergeDoc::DoSaveAs(LPCTSTR szPath, bool &bSaveSuccess, int nBuffer)
1773 String strSavePath(szPath);
1775 // use a temp packer
1776 // first copy the m_pInfoUnpacker
1777 // if an error arises during packing, change and take a "do nothing" packer
1778 PackingInfo infoTempUnpacker = *m_pInfoUnpacker;
1780 bSaveSuccess = false;
1781 // false as long as the user is not satisfied
1782 // true if saving succeeds, even with another filename, or if the user cancels
1783 bool result = false;
1784 // the error code from the latest save operation,
1785 // or SAVE_DONE when the save succeeds
1786 // TODO: Shall we return this code in addition to bSaveSuccess ?
1787 int nSaveErrorCode = SAVE_DONE;
1789 // Use SAVE_NO_FILENAME to prevent asking about error
1790 nSaveErrorCode = SAVE_NO_FILENAME;
1792 // Loop until user succeeds saving or cancels
1795 result = TrySaveAs(strSavePath, nSaveErrorCode, sError, nBuffer, &infoTempUnpacker);
1798 // Saving succeeded with given/selected filename
1799 if (nSaveErrorCode == SAVE_DONE)
1801 m_pSaveFileInfo[nBuffer]->Update(strSavePath);
1802 m_filePaths[nBuffer] = strSavePath;
1803 m_pRescanFileInfo[nBuffer]->Update(m_filePaths[nBuffer]);
1804 UpdateHeaderPath(nBuffer);
1805 bSaveSuccess = true;
1812 * @brief Get left->right info for a moved line (apparent line number)
1814 int CMergeDoc::RightLineInMovedBlock(int nBuffer, int apparentLeftLine)
1816 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentLeftLine) & LF_MOVED))
1819 int realLeftLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentLeftLine);
1820 int realRightLine = -1;
1821 if (m_diffWrapper.GetDetectMovedBlocks())
1823 realRightLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realLeftLine,
1824 MovedLines::SIDE::RIGHT);
1826 if (realRightLine != -1)
1827 return m_ptBuf[nBuffer + 1]->ComputeApparentLine(realRightLine);
1833 * @brief Get right->left info for a moved line (apparent line number)
1835 int CMergeDoc::LeftLineInMovedBlock(int nBuffer, int apparentRightLine)
1837 if (!(m_ptBuf[nBuffer]->GetLineFlags(apparentRightLine) & LF_MOVED))
1840 int realRightLine = m_ptBuf[nBuffer]->ComputeRealLine(apparentRightLine);
1841 int realLeftLine = -1;
1842 if (m_diffWrapper.GetDetectMovedBlocks())
1844 realLeftLine = m_diffWrapper.GetMovedLines(nBuffer)->LineInBlock(realRightLine,
1845 MovedLines::SIDE::LEFT);
1847 if (realLeftLine != -1)
1848 return m_ptBuf[nBuffer - 1]->ComputeApparentLine(realLeftLine);
1854 * @brief Save modified documents.
1855 * This function asks if user wants to save modified documents. We also
1856 * allow user to cancel the closing.
1858 * There is a special trick avoiding showing two save-dialogs, as MFC framework
1859 * sometimes calls this function twice. We use static counter for these calls
1860 * and if we already have saving in progress (counter == 1) we skip the new
1863 * @return true if docs are closed, false if closing is cancelled.
1865 BOOL CMergeDoc::SaveModified()
1872 if (PromptAndSaveIfNeeded(true))
1885 * @brief Sets the current difference.
1886 * @param [in] nDiff Difference to set as current difference.
1888 void CMergeDoc::SetCurrentDiff(int nDiff)
1890 if (nDiff >= 0 && nDiff <= m_diffList.LastSignificantDiff())
1897 * @brief Take care of rescanning document.
1899 * Update view and restore cursor and scroll position after
1900 * rescanning document.
1901 * @param [in] bForced If true rescan cannot be suppressed
1903 void CMergeDoc::FlushAndRescan(bool bForced /* =false */)
1905 // Ignore suppressing when forced rescan
1907 if (!m_bEnableRescan) return;
1909 CWaitCursor waitstatus;
1911 CMergeEditView *pActiveView = GetActiveMergeView();
1913 // store cursors and hide caret
1914 ForEachView([](auto& pView) { pView->PushCursors(); });
1915 pActiveView->HideCursor();
1917 bool bBinary = false;
1918 IDENTLEVEL identical = IDENTLEVEL::NONE;
1919 int nRescanResult = Rescan(bBinary, identical, bForced);
1921 // restore cursors and caret
1922 ForEachView([](auto& pView) { pView->PopCursors(); });
1923 pActiveView->ShowCursor();
1925 ForEachView(pActiveView->m_nThisPane, [](auto& pView) {
1926 // because of ghostlines, m_nTopLine may differ just after Rescan
1927 // scroll both views to the same top line
1928 pView->UpdateSiblingScrollPos(false);
1930 // make sure we see the cursor from the curent view
1931 pActiveView->EnsureVisible(pActiveView->GetCursorPos());
1934 UpdateAllViews(nullptr);
1936 // Show possible error after updating screen
1937 if (nRescanResult != RESCAN_SUPPRESSED)
1938 ShowRescanError(nRescanResult, identical);
1939 m_LastRescan = COleDateTime::GetCurrentTime();
1943 * @brief Saves both files
1945 void CMergeDoc::OnFileSave()
1947 // We will need to know if either of the originals actually changed
1948 // so we know whether to update the diff status
1949 bool bChangedOriginal = false;
1951 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1953 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1955 // (why we don't use return value of DoSave)
1956 // DoSave will return true if it wrote to something successfully
1957 // but we have to know if it overwrote the original file
1958 bool bSaveOriginal = false;
1959 DoSave(m_filePaths[nBuffer].c_str(), bSaveOriginal, nBuffer );
1961 bChangedOriginal = true;
1965 // If either of the actual source files being compared was changed
1966 // we need to update status in the dir view
1967 if (bChangedOriginal)
1969 // If DirDoc contains diffs
1970 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
1972 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1974 if (m_bEditAfterRescan[nBuffer])
1976 FlushAndRescan(false);
1981 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
1982 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
1983 m_nTrivialDiffs, bIdentical);
1988 void CMergeDoc::DoFileSave(int nBuffer)
1990 bool bSaveSuccess = false;
1991 bool bModified = false;
1993 if (m_ptBuf[nBuffer]->IsModified() && !m_ptBuf[nBuffer]->GetReadOnly())
1996 DoSave(m_filePaths[nBuffer].c_str(), bSaveSuccess, nBuffer );
1999 // If file were modified and saving succeeded,
2000 // update status on dir view
2001 if (bModified && bSaveSuccess)
2003 // If DirDoc contains compare results
2004 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2006 for (int nBuffer1 = 0; nBuffer1 < m_nBuffers; nBuffer1++)
2008 if (m_bEditAfterRescan[nBuffer1])
2010 FlushAndRescan(false);
2015 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2016 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2017 m_nTrivialDiffs, bIdentical);
2023 * @brief Saves left-side file
2025 void CMergeDoc::OnFileSaveLeft()
2031 * @brief Saves middle-side file
2033 void CMergeDoc::OnFileSaveMiddle()
2039 * @brief Saves right-side file
2041 void CMergeDoc::OnFileSaveRight()
2043 DoFileSave(m_nBuffers - 1);
2047 * @brief Saves left-side file with name asked
2049 void CMergeDoc::OnFileSaveAsLeft()
2051 bool bSaveResult = false;
2052 DoSaveAs(m_filePaths.GetLeft().c_str(), bSaveResult, 0);
2056 * @brief Called when "Save middle (as...)" item is updated
2058 void CMergeDoc::OnUpdateFileSaveAsMiddle(CCmdUI* pCmdUI)
2060 pCmdUI->Enable(m_nBuffers == 3);
2064 * @brief Saves right-side file with name asked
2066 void CMergeDoc::OnFileSaveAsMiddle()
2068 bool bSaveResult = false;
2069 DoSaveAs(m_filePaths.GetMiddle().c_str(), bSaveResult, 1);
2073 * @brief Saves right-side file with name asked
2075 void CMergeDoc::OnFileSaveAsRight()
2077 bool bSaveResult = false;
2078 DoSaveAs(m_filePaths.GetRight().c_str(), bSaveResult, m_nBuffers - 1);
2082 * @brief Update diff-number pane text in file compare.
2083 * The diff number pane shows selected difference/amount of differences when
2084 * there is difference selected. If there is no difference selected, then
2085 * the panel shows amount of differences. If there are ignored differences,
2086 * those are not count into numbers.
2087 * @param [in] pCmdUI UI component to update.
2089 void CMergeDoc::OnUpdateStatusNum(CCmdUI* pCmdUI)
2091 TCHAR sIdx[32] = { 0 };
2092 TCHAR sCnt[32] = { 0 };
2094 const int nDiffs = m_diffList.GetSignificantDiffs();
2096 // Files are identical - show text "Identical"
2100 // There are differences, but no selected diff
2101 // - show amount of diffs
2102 else if (GetCurrentDiff() < 0)
2104 s = nDiffs == 1 ? _("1 Difference Found") : _("%1 Differences Found");
2105 _itot_s(nDiffs, sCnt, 10);
2106 strutils::replace(s, _T("%1"), sCnt);
2109 // There are differences and diff selected
2110 // - show diff number and amount of diffs
2113 s = _("Difference %1 of %2");
2114 const int signInd = m_diffList.GetSignificantIndex(GetCurrentDiff());
2115 _itot_s(signInd + 1, sIdx, 10);
2116 strutils::replace(s, _T("%1"), sIdx);
2117 _itot_s(nDiffs, sCnt, 10);
2118 strutils::replace(s, _T("%2"), sCnt);
2120 pCmdUI->SetText(s.c_str());
2124 * @brief Update plugin name
2125 * @param [in] pCmdUI UI component to update.
2127 void CMergeDoc::OnUpdatePluginName(CCmdUI* pCmdUI)
2130 if (m_pInfoUnpacker && !m_pInfoUnpacker->m_PluginName.empty())
2131 pluginNames += m_pInfoUnpacker->m_PluginName + _T("&");
2132 PrediffingInfo prediffer;
2133 GetPrediffer(&prediffer);
2134 if (!prediffer.m_PluginName.empty())
2135 pluginNames += prediffer.m_PluginName + _T("&");
2136 pCmdUI->SetText(pluginNames.substr(0, pluginNames.length() - 1).c_str());
2140 * @brief Change number of diff context lines
2142 void CMergeDoc::OnDiffContext(UINT nID)
2146 case ID_VIEW_DIFFCONTEXT_0:
2147 m_nDiffContext = 0; break;
2148 case ID_VIEW_DIFFCONTEXT_1:
2149 m_nDiffContext = 1; break;
2150 case ID_VIEW_DIFFCONTEXT_3:
2151 m_nDiffContext = 3; break;
2152 case ID_VIEW_DIFFCONTEXT_5:
2153 m_nDiffContext = 5; break;
2154 case ID_VIEW_DIFFCONTEXT_7:
2155 m_nDiffContext = 7; break;
2156 case ID_VIEW_DIFFCONTEXT_9:
2157 m_nDiffContext = 9; break;
2158 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2159 m_nDiffContext = -m_nDiffContext - 1; break;
2160 case ID_VIEW_DIFFCONTEXT_ALL:
2161 if (m_nDiffContext >= 0)
2162 m_nDiffContext = -m_nDiffContext - 1;
2164 case ID_VIEW_DIFFCONTEXT_INVERT:
2165 m_bInvertDiffContext = !m_bInvertDiffContext;
2168 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2169 GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2170 FlushAndRescan(true);
2174 * @brief Swap context enable for 3 file compares
2176 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2180 pCmdUI->Enable(true);
2184 pCmdUI->Enable(false);
2189 * @brief Update number of diff context lines
2191 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2194 switch (pCmdUI->m_nID)
2196 case ID_VIEW_DIFFCONTEXT_0:
2197 bCheck = (m_nDiffContext == 0); break;
2198 case ID_VIEW_DIFFCONTEXT_1:
2199 bCheck = (m_nDiffContext == 1); break;
2200 case ID_VIEW_DIFFCONTEXT_3:
2201 bCheck = (m_nDiffContext == 3); break;
2202 case ID_VIEW_DIFFCONTEXT_5:
2203 bCheck = (m_nDiffContext == 5); break;
2204 case ID_VIEW_DIFFCONTEXT_7:
2205 bCheck = (m_nDiffContext == 7); break;
2206 case ID_VIEW_DIFFCONTEXT_9:
2207 bCheck = (m_nDiffContext == 9); break;
2208 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2209 bCheck = false; break;
2210 case ID_VIEW_DIFFCONTEXT_INVERT:
2211 bCheck = m_bInvertDiffContext; break;
2213 bCheck = (m_nDiffContext < 0); break;
2215 pCmdUI->SetCheck(bCheck);
2216 pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2220 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2222 * @note After PrimeTextBuffers(), all buffers should have the same length.
2224 void CMergeDoc::PrimeTextBuffers()
2227 m_nTrivialDiffs = 0;
2229 int nDiffCount = m_diffList.GetSize();
2232 // walk the diff list and calculate numbers of extra lines to add
2233 int extras[3] = {0, 0, 0}; // extra lines added to each view
2234 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2236 // resize m_aLines once for each view
2237 UINT lcount[3] = {0, 0, 0};
2238 UINT lcountnew[3] = {0, 0, 0};
2241 for (file = 0; file < m_nBuffers; file++)
2243 lcount[file] = m_ptBuf[file]->GetLineCount();
2244 lcountnew[file] = lcount[file] + extras[file];
2245 lcountmax = max(lcountmax, lcountnew[file]);
2247 for (file = 0; file < m_nBuffers; file++)
2249 m_ptBuf[file]->m_aLines.resize(lcountmax);
2252 // walk the diff list backward, move existing lines to proper place,
2253 // add ghost lines, and set flags
2254 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2257 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2259 // move matched lines after curDiff
2260 int nline[3] = { 0, 0, 0 };
2261 for (file = 0; file < m_nBuffers; file++)
2262 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2263 // Matched lines should really match...
2264 // But matched lines after last diff may differ because of empty last line (see function's note)
2265 if (nDiff < nDiffCount - 1)
2266 ASSERT(nline[0] == nline[1]);
2269 for (file = 0; file < m_nBuffers; file++)
2271 // Move all lines after current diff down as far as needed
2272 // for any ghost lines we're about to insert
2273 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2274 lcountnew[file] -= nline[file];
2275 lcount[file] -= nline[file];
2276 // move unmatched lines and add ghost lines
2277 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2278 nmaxline = max(nmaxline, nline[file]);
2281 for (file = 0; file < m_nBuffers; file++)
2283 DWORD dflag = LF_GHOST;
2284 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2286 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2287 int nextra = nmaxline - nline[file];
2290 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2291 for (int i = 1; i <= nextra; i++)
2292 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2294 lcountnew[file] -= nmaxline;
2296 lcount[file] -= nline[file];
2299 // set dbegin, dend, blank, and line flags
2300 curDiff.dbegin = lcountnew[0];
2313 curDiff.dend = lcountnew[0]+nmaxline-1;
2314 for (file = 0; file < m_nBuffers; file++)
2316 curDiff.blank[file] = -1;
2317 int nextra = nmaxline - nline[file];
2318 if (nmaxline > nline[file])
2320 // more lines on left, ghost lines on right side
2321 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2327 for (file = 0; file < m_nBuffers; file++)
2331 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2333 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2335 // set diff or trivial flag
2336 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2337 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2339 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2340 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2344 // ghost lines are already inserted (and flagged)
2345 // ghost lines opposite to trivial lines are ghost and trivial
2346 if (curDiff.op == OP_TRIVIAL)
2347 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2353 } // switch (curDiff.op)
2354 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2355 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2357 m_diffList.ConstructSignificantChain();
2360 // Note: By this point all `m_ptBuf[]` buffers must have the same
2361 // number of line entries; eventual buffer processing typically
2362 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2364 for (file = 0; file < m_nBuffers; file++)
2366 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2370 for (file = 0; file < m_nBuffers; file++)
2371 m_ptBuf[file]->FinishLoading();
2375 * @brief Checks if file has changed since last update (save or rescan).
2376 * @param [in] szPath File to check
2377 * @param [in] dfi Previous fileinfo of file
2378 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2379 * @param [in] nBuffer Index (0-based) of buffer
2380 * @return true if file is changed.
2382 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2383 bool bSave, int nBuffer)
2385 DiffFileInfo *fileInfo = nullptr;
2386 bool bFileChanged = false;
2387 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2389 if (bIgnoreSmallDiff)
2390 tolerance = SmallTimeDiff; // From MainFrm.h
2393 fileInfo = m_pSaveFileInfo[nBuffer].get();
2395 fileInfo = m_pRescanFileInfo[nBuffer].get();
2397 // We assume file existed, so disappearing means removal
2398 if (!dfi.Update(szPath))
2399 return FileChange::Removed;
2401 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2402 if (timeDiff < 0) timeDiff = -timeDiff;
2403 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2405 bFileChanged = true;
2409 return FileChange::Changed;
2411 return FileChange::NoChange;
2414 void CMergeDoc::HideLines()
2419 if (m_nDiffContext < 0)
2421 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2425 int nLineCount = 0x7fffffff;
2426 for (file = 0; file < m_nBuffers; file++)
2428 if (nLineCount > m_ptBuf[file]->GetLineCount())
2429 nLineCount = m_ptBuf[file]->GetLineCount();
2432 for (nLine = 0; nLine < nLineCount;)
2434 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2435 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2437 for (file = 0; file < m_nBuffers; file++)
2438 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2443 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2444 for (; nLine2 < nLine; nLine2++)
2446 for (file = 0; file < m_nBuffers; file++)
2447 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2450 for (; nLine < nLineCount; nLine++)
2452 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2453 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2455 for (file = 0; file < m_nBuffers; file++)
2456 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2459 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2460 for (; nLine < nLineEnd2; nLine++)
2462 for (file = 0; file < m_nBuffers; file++)
2463 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2464 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2465 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2466 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2471 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2475 * @brief Asks and then saves modified files.
2477 * This function saves modified files. Dialog is shown for user to select
2478 * modified file(s) one wants to save or discard changed. Cancelling of
2479 * save operation is allowed unless denied by parameter. After successfully
2480 * save operation file statuses are updated to directory compare.
2481 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2482 * @return true if user selected "OK" so next operation can be
2483 * executed. If false user choosed "Cancel".
2484 * @note If filename is empty, we assume scratchpads are saved,
2485 * so instead of filename, description is shown.
2486 * @todo If we have filename and description for file, what should
2487 * we do after saving to different filename? Empty description?
2488 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2490 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2492 bool bLModified = false, bMModified = false, bRModified = false;
2494 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2496 if (m_nBuffers == 3)
2498 bLModified = m_ptBuf[0]->IsModified();
2499 bMModified = m_ptBuf[1]->IsModified();
2500 bRModified = m_ptBuf[2]->IsModified();
2504 bLModified = m_ptBuf[0]->IsModified();
2505 bRModified = m_ptBuf[1]->IsModified();
2507 if (!bLModified && !bMModified && !bRModified)
2511 dlg.DoAskFor(bLModified, bMModified, bRModified);
2513 dlg.m_bDisableCancel = true;
2514 if (!m_filePaths.GetLeft().empty())
2516 if (theApp.m_strSaveAsPath.empty())
2517 dlg.m_sLeftFile = m_filePaths.GetLeft();
2519 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2522 dlg.m_sLeftFile = m_strDesc[0];
2523 if (m_nBuffers == 3)
2525 if (!m_filePaths.GetMiddle().empty())
2527 if (theApp.m_strSaveAsPath.empty())
2528 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2530 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2533 dlg.m_sMiddleFile = m_strDesc[1];
2535 if (!m_filePaths.GetRight().empty())
2537 if (theApp.m_strSaveAsPath.empty())
2538 dlg.m_sRightFile = m_filePaths.GetRight();
2540 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2543 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2545 if (dlg.DoModal() == IDOK)
2547 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2549 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2553 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2555 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2559 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2561 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2570 // If file were modified and saving was successfull,
2571 // update status on dir view
2572 if ((bLModified && bLSaveSuccess) ||
2573 (bMModified && bMSaveSuccess) ||
2574 (bRModified && bRSaveSuccess))
2576 // If directory compare has results
2577 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2579 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2580 FlushAndRescan(false);
2582 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2583 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2584 m_nTrivialDiffs, bIdentical);
2591 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2592 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2594 // if we did not rescan during the request timeOut, Rescan
2595 // else we did Rescan after the request, so do nothing
2596 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2597 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2598 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2603 * @brief We have two child views (left & right), so we keep pointers directly
2604 * at them (the MFC view list doesn't have them both)
2606 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2609 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2610 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2614 void CMergeDoc::RemoveMergeViews(int nGroup)
2617 for (; nGroup < m_nGroups - 1; nGroup++)
2619 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2621 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2622 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2629 * @brief DirDoc gives us its identity just after it creates us
2631 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2633 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2634 m_pDirDoc = pDirDoc;
2638 * @brief Return pointer to parent frame
2640 CMergeEditFrame * CMergeDoc::GetParentFrame()
2642 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2646 * @brief DirDoc is closing
2648 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2650 ASSERT(m_pDirDoc == pDirDoc);
2651 m_pDirDoc = nullptr;
2652 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2656 * @brief DirDoc commanding us to close
2658 bool CMergeDoc::CloseNow()
2660 // Allow user to cancel closing
2661 if (!PromptAndSaveIfNeeded(true))
2664 GetParentFrame()->CloseNow();
2669 * @brief Loads file to buffer and shows load-errors
2670 * @param [in] sFileName File to open
2671 * @param [in] nBuffer Index (0-based) of buffer to load
2672 * @param [out] readOnly whether file is read-only
2673 * @param [in] encoding encoding used
2674 * @return Tells if files were loaded successfully
2675 * @sa CMergeDoc::OpenDocs()
2677 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2680 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2682 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2683 m_filePaths[nBuffer] = sFileName;
2685 CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2687 retVal = pBuf->LoadFromFile(sFileName, m_pInfoUnpacker.get(),
2688 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2690 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2691 // it left the pBuf in a valid (but empty) state via a call to InitNew
2693 if (FileLoadResult::IsOkImpure(retVal))
2695 // File loaded, and multiple EOL types in this file
2696 FileLoadResult::SetMainOk(retVal);
2698 // If mixed EOLs are not enabled, enable them for this doc.
2699 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2701 pBuf->SetMixedEOL(true);
2705 if (FileLoadResult::IsError(retVal))
2707 // Error from Unifile/system
2708 if (!sOpenError.IsEmpty())
2709 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2711 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2712 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2714 else if (FileLoadResult::IsErrorUnpack(retVal))
2716 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2717 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2723 * @brief Check if specified codepage number is valid for WinMerge Editor.
2724 * @param [in] cp Codepage number to check.
2725 * @return true if codepage is valid, false otherwise.
2727 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2729 if (cp == 0) // 0 is our signal value for invalid
2731 return GetEncodingNameFromCodePage(cp) != nullptr;
2735 * @brief Sanity check file's specified codepage.
2736 * This function checks if file's specified codepage is valid for WinMerge
2737 * editor and if not resets the codepage to default.
2738 * @param [in,out] fileinfo Class containing file's codepage.
2740 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2742 if (fileinfo.encoding.m_unicoding == ucr::NONE
2743 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2745 int cp = ucr::getDefaultCodepage();
2746 if (!IsValidCodepageForMergeEditor(cp))
2748 fileinfo.encoding.SetCodepage(cp);
2753 * @brief Loads one file from disk and updates file infos.
2754 * @param [in] index Index of file in internal buffers.
2755 * @param [in] filename File's name.
2756 * @param [in] readOnly Is file read-only?
2757 * @param [in] encoding File's encoding.
2758 * @return One of FileLoadResult values.
2760 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2761 const FileTextEncoding & encoding)
2763 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2765 m_strDesc[index] = strDesc;
2766 if (!filename.empty())
2768 if (strDesc.empty())
2769 m_nBufferType[index] = BUFFERTYPE::NORMAL;
2771 m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2772 m_pSaveFileInfo[index]->Update(filename);
2773 m_pRescanFileInfo[index]->Update(filename);
2775 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2776 if (FileLoadResult::IsLossy(loadSuccess))
2778 m_ptBuf[index]->FreeAll();
2779 loadSuccess = LoadFile(filename.c_str(), index, readOnly,
2780 codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1));
2785 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2786 m_ptBuf[index]->InitNew();
2787 m_ptBuf[index]->m_encoding = encoding;
2788 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2789 loadSuccess = FileLoadResult::FRESULT_OK;
2794 void CMergeDoc::SetTableProperties()
2796 struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
2797 auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
2799 const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
2800 FileFilterHelper filterCSV, filterTSV, filterDSV;
2801 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2802 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2803 if (!csvFilePattern.empty())
2805 filterCSV.UseMask(true);
2806 filterCSV.SetMask(csvFilePattern);
2807 if (filterCSV.includeFile(path))
2808 return { true, ',', quote, allowNewlineIQuotes };
2810 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2811 if (!tsvFilePattern.empty())
2813 filterTSV.UseMask(true);
2814 filterTSV.SetMask(tsvFilePattern);
2815 if (filterTSV.includeFile(path))
2816 return { true, '\t', quote, allowNewlineIQuotes };
2818 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2819 if (!dsvFilePattern.empty())
2821 filterDSV.UseMask(true);
2822 filterDSV.SetMask(dsvFilePattern);
2823 if (filterDSV.includeFile(path))
2824 return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
2826 if (enableTableEditing.value_or(false))
2829 if (dlg.DoModal() == IDOK)
2830 return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
2832 return { false, 0, 0, false };
2835 TableProps tableProps[3] = {};
2836 int nTableFileIndex = -1;
2837 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2840 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2841 tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2843 tableProps[nBuffer] = tableProps[nBuffer - 1];
2844 if (tableProps[nBuffer].istable)
2845 nTableFileIndex = nBuffer;
2847 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2849 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2851 int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
2852 m_ptBuf[nBuffer]->SetTableEditing(true);
2853 m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
2854 m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
2855 m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
2856 m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
2857 m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
2861 m_ptBuf[nBuffer]->SetTableEditing(false);
2867 * @brief Loads files and does initial rescan.
2868 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2869 * @param bRO [in] Is left/middle/right file read-only
2870 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2871 * @todo Options are still read from CMainFrame, this will change
2872 * @sa CMainFrame::ShowTextMergeDoc()
2874 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2875 const bool bRO[], const String strDesc[])
2877 IDENTLEVEL identical = IDENTLEVEL::NONE;
2878 int nRescanResult = RESCAN_OK;
2880 FileLocation fileloc[3];
2882 std::copy_n(ifileloc, 3, fileloc);
2884 // Filter out invalid codepages, or editor will display all blank
2885 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2886 SanityCheckCodepage(fileloc[nBuffer]);
2890 curUndo = undoTgt.begin();
2892 // Prevent displaying views during LoadFile
2893 // Note : attach buffer again only if both loads succeed
2894 m_strBothFilenames.erase();
2896 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2898 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2900 // clear undo buffers
2901 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2904 m_ptBuf[nBuffer]->FreeAll ();
2906 // build the text being filtered, "|" separates files as it is forbidden in filenames
2907 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2909 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2913 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2915 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2916 fileloc[nBuffer].encoding);
2919 SetTableProperties();
2921 const bool bFiltersEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
2923 // scratchpad : we don't call LoadFile, so
2924 // we need to initialize the unpacker as a "do nothing" one
2925 if (bFiltersEnabled)
2927 if (std::count(m_nBufferType, m_nBufferType + m_nBuffers, BUFFERTYPE::UNNAMED) == m_nBuffers)
2929 m_pInfoUnpacker->Initialize(PLUGIN_MODE::PLUGIN_MANUAL);
2933 // Bail out if either side failed
2934 if (std::find_if(nSuccess, nSuccess + m_nBuffers, [](DWORD d){return !FileLoadResult::IsOk(d);} ) != nSuccess + m_nBuffers)
2936 CMergeEditFrame *pFrame = GetParentFrame();
2937 if (pFrame != nullptr)
2939 // Use verify macro to trap possible error in debug.
2940 VERIFY(pFrame->DestroyWindow());
2945 // Warn user if file load was lossy (bad encoding)
2947 int nLossyBuffers = 0;
2948 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2950 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2952 // TODO: It would be nice to report how many lines were lossy
2953 // we did calculate those numbers when we loaded the files, in the text stats
2955 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2959 if (nLossyBuffers > 1)
2960 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2962 if (nLossyBuffers > 0)
2964 if (m_pEncodingErrorBar == nullptr)
2966 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2967 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2969 m_pEncodingErrorBar->SetText(LoadResString(idres));
2970 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2973 ForEachView([](auto& pView) {
2974 // Now buffers data are valid
2975 pView->AttachToBuffer();
2976 // Currently there is only one set of syntax colors, which all documents & views share
2977 pView->SetColorContext(theApp.GetMainSyntaxColors());
2978 // Currently there is only one set of markers, which all documents & views share
2979 pView->SetMarkersContext(theApp.GetMainMarkers());
2981 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2983 // Set read-only statuses
2984 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2987 // Check the EOL sensitivity option (do it before Rescan)
2988 DIFFOPTIONS diffOptions = {0};
2989 m_diffWrapper.GetOptions(&diffOptions);
2990 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2992 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2993 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2996 if (nBuffer < m_nBuffers)
2998 // Options and files not are not compatible :
2999 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
3000 // All lines will differ, that is not very interesting and probably not wanted.
3001 // Propose to turn off the option 'sensitive to EOL'
3002 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
3003 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
3005 diffOptions.bIgnoreEol = true;
3006 m_diffWrapper.SetOptions(&diffOptions);
3008 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
3009 const int nFormerResult = dlg.GetFormerResult();
3010 if (nFormerResult != -1)
3012 // "Don't ask this question again" checkbox is checked
3013 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
3019 // Define the prediffer
3020 PackingInfo * infoUnpacker = nullptr;
3021 PrediffingInfo * infoPrediffer = nullptr;
3022 if (bFiltersEnabled && m_pDirDoc != nullptr)
3024 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3025 m_diffWrapper.SetPrediffer(infoPrediffer);
3026 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3029 bool bBinary = false;
3030 nRescanResult = Rescan(bBinary, identical);
3032 // Open filed if rescan succeed and files are not binaries
3033 if (nRescanResult == RESCAN_OK)
3035 // set the document types
3036 // Warning : it is the first thing to do (must be done before UpdateView,
3037 // or any function that calls UpdateView, like SelectDiff)
3038 // Note: If option enabled, and another side type is not recognized,
3039 // we use recognized type for unrecognized side too.
3044 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3046 if (bFiltersEnabled && m_pInfoUnpacker->m_textType.length())
3047 sext[nBuffer] = m_pInfoUnpacker->m_textType;
3049 sext[nBuffer] = GetFileExt(fileloc[nBuffer].filepath.c_str(), m_strDesc[nBuffer].c_str());
3050 ForEachView(nBuffer, [&](auto& pView) {
3051 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3052 if (bTyped[nBuffer])
3053 paneTyped = nBuffer;
3057 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3059 if (bTyped[0] != bTyped[nBuffer])
3063 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3064 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3066 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3069 m_ptBuf[0]->GetLine(0, sFirstLine);
3070 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3072 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3077 if (syntaxHLEnabled)
3079 CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3080 ForEachView([&bTyped, enuType](auto& pView) {
3081 if (!bTyped[pView->m_nThisPane])
3082 pView->SetTextType(enuType);
3086 int nNormalBuffer = 0;
3087 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3089 // set the frame window header
3090 UpdateHeaderPath(nBuffer);
3092 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3094 if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3095 (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3102 // Inform user that files are identical
3103 // Don't show message if new buffers created
3104 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3106 ShowRescanError(nRescanResult, identical);
3109 // Exit if files are identical should only work for the first
3110 // comparison and must be disabled afterward.
3111 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3115 // CMergeDoc::Rescan fails if files do not exist on both sides
3116 // or the really arcane case that the temp files couldn't be created,
3117 // which is too obscure to bother reporting if you can't write to
3118 // your temp directory, doing nothing is graceful enough for that).
3119 ShowRescanError(nRescanResult, identical);
3120 GetParentFrame()->DestroyWindow();
3124 // Force repaint of location pane to update it in case we had some warning
3125 // dialog visible and it got painted before files were loaded
3126 if (m_pView[0][0] != nullptr)
3127 m_pView[0][0]->RepaintLocationPane();
3132 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex)
3136 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3137 if (nPane < 0 || nPane >= m_nBuffers)
3140 if (nLineIndex == -1)
3142 // scroll to first diff
3143 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3144 m_diffList.HasSignificantDiffs())
3146 int nDiff = m_diffList.FirstSignificantDiff();
3148 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3149 m_pView[0][nPane]->SetActivePane();
3153 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, false, nPane);
3156 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3158 if (!PromptAndSaveIfNeeded(true))
3161 FileLocation fileloc[3];
3164 for (int pane = 0; pane < m_nBuffers; pane++)
3166 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3167 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3168 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3169 fileloc[pane].setPath(m_filePaths[pane]);
3171 std::copy_n(m_strDesc, m_nBuffers, strDesc);
3173 strDesc[nBuffer] = _T("");
3174 fileloc[nBuffer].setPath(path);
3175 fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3177 if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3178 MoveOnLoad(nBuffer, nLineIndex);
3182 * @brief Re-load a document.
3183 * This methods re-loads the file compare document. The re-loaded document is
3184 * one side of the file compare.
3185 * @param [in] index The document to re-load.
3186 * @return Open result code.
3188 void CMergeDoc::RefreshOptions()
3190 DIFFOPTIONS options = {0};
3192 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3194 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3195 Options::DiffOptions::Load(GetOptionsMgr(), options);
3197 m_diffWrapper.SetOptions(&options);
3199 // Refresh view options
3200 ForEachView([](auto& pView) { pView->RefreshOptions(); });
3204 * @brief Write path and filename to headerbar
3205 * @note SetText() does not repaint unchanged text
3207 void CMergeDoc::UpdateHeaderPath(int pane)
3209 CMergeEditFrame *pf = GetParentFrame();
3210 ASSERT(pf != nullptr);
3212 bool bChanges = false;
3214 if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3215 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3217 sText = m_strDesc[pane];
3221 sText = m_filePaths[pane];
3222 if (m_pDirDoc != nullptr)
3224 m_pDirDoc->ApplyDisplayRoot(pane, sText);
3227 bChanges = m_ptBuf[pane]->IsModified();
3230 sText.insert(0, _T("* "));
3232 pf->GetHeaderInterface()->SetText(pane, sText);
3238 * @brief Paint differently the headerbar of the active view
3240 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3242 CMergeEditFrame *pf = GetParentFrame();
3243 ASSERT(pf != nullptr);
3244 pf->GetHeaderInterface()->SetActive(pane, bActivate);
3248 * @brief Set detect/not detect Moved Blocks
3250 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3252 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3255 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3256 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3261 * @brief Check if given buffer has mixed EOL style.
3262 * @param [in] nBuffer Buffer to check.
3263 * @return true if buffer's EOL style is mixed, false otherwise.
3265 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3267 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3268 return pBuf->IsMixedEOL();
3271 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3273 m_bEditAfterRescan[nBuffer] = true;
3276 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3278 if (nBuffer >= 0 && nBuffer < m_nBuffers)
3279 return m_bEditAfterRescan[nBuffer];
3281 for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3283 if (m_bEditAfterRescan[nBuffer])
3291 * @brief Update document filenames to title
3293 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3295 String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc);
3296 CDocument::SetTitle(sTitle.c_str());
3300 * @brief Update any resources necessary after a GUI language change
3302 void CMergeDoc::UpdateResources()
3304 if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3305 m_strDesc[0] = _("Untitled left");
3306 if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3307 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3308 if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3309 m_strDesc[1] = _("Untitled middle");
3310 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3311 UpdateHeaderPath(nBuffer);
3313 GetParentFrame()->UpdateResources();
3314 ForEachView([](auto& pView) { pView->UpdateResources(); });
3317 // Return current word breaking break type setting (whitespace only or include punctuation)
3318 bool CMergeDoc::GetBreakType() const
3320 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3324 // Return true to do line diff colors at the byte level (false to do them at word level)
3325 bool CMergeDoc::GetByteColoringOption() const
3327 // color at byte level if 'break_on_words' option not set
3328 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3332 /// Swap files and update views
3333 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3335 if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3338 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3340 int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3341 int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3342 m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3343 m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3347 // Swap buffers and so on
3348 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3349 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3350 std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3351 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3352 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3353 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3354 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3355 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3357 m_filePaths.Swap(nFromIndex, nToIndex);
3358 m_diffList.Swap(nFromIndex, nToIndex);
3359 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3360 swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3362 ClearWordDiffCache();
3364 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3366 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3367 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3368 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3371 UpdateHeaderPath(nBuffer);
3373 GetParentFrame()->UpdateSplitter();
3374 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3376 UpdateAllViews(nullptr);
3381 * @brief Display unpacker dialog to user & handle user's choices
3383 bool CMergeDoc::OpenWithUnpackerDialog()
3385 // let the user choose a handler
3386 CSelectUnpackerDlg dlg(m_filePaths[0], nullptr);
3387 // create now a new infoUnpacker to initialize the manual/automatic flag
3388 PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_AUTO);
3389 dlg.SetInitialInfoHandler(&infoUnpacker);
3391 if (dlg.DoModal() == IDOK)
3393 infoUnpacker = dlg.GetInfoHandler();
3394 Merge7zFormatMergePluginScope scope(&infoUnpacker);
3395 if (HasZipSupport() && std::count_if(m_filePaths.begin(), m_filePaths.end(), ArchiveGuessFormat) == m_nBuffers)
3397 DWORD dwFlags[3] = {FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU};
3398 GetMainFrame()->DoFileOpen(&m_filePaths, dwFlags, m_strDesc, _T(""),
3399 GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS), nullptr, _T(""), &infoUnpacker);
3404 SetUnpacker(&infoUnpacker);
3416 * @brief Reloads the opened files
3418 void CMergeDoc::OnFileReload()
3420 if (!PromptAndSaveIfNeeded(true))
3423 FileLocation fileloc[3];
3425 for (int pane = 0; pane < m_nBuffers; pane++)
3427 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3428 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3429 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3430 fileloc[pane].setPath(m_filePaths[pane]);
3432 CPoint pt = GetActiveMergeView()->GetCursorPos();
3433 if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3434 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3438 * @brief Display encodings to user
3440 void CMergeDoc::OnFileEncoding()
3442 DoFileEncodingDialog();
3445 void CMergeDoc::OnCtxtOpenWithUnpacker()
3447 OpenWithUnpackerDialog();
3450 void CMergeDoc::OnBnClickedFileEncoding()
3452 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3454 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3455 DoFileEncodingDialog();
3458 void CMergeDoc::OnBnClickedPlugin()
3460 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3462 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3463 OpenWithUnpackerDialog();
3466 void CMergeDoc::OnBnClickedHexView()
3468 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3471 void CMergeDoc::OnOK()
3473 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3475 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3478 void CMergeDoc::OnFileRecompareAsText()
3480 m_bEnableTableEditing = false;
3481 PackingInfo infoUnpacker;
3482 SetUnpacker(&infoUnpacker);
3486 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3488 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode == PLUGIN_MODE::PLUGIN_BUILTIN_XML ||
3489 m_ptBuf[0]->GetTableEditing());
3492 void CMergeDoc::OnFileRecompareAsTable()
3494 m_bEnableTableEditing = true;
3495 PackingInfo infoUnpacker;
3496 SetUnpacker(&infoUnpacker);
3500 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3502 pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3505 void CMergeDoc::OnFileRecompareAsXML()
3507 m_bEnableTableEditing = false;
3508 PackingInfo infoUnpacker(PLUGIN_MODE::PLUGIN_BUILTIN_XML);
3509 SetUnpacker(&infoUnpacker);
3513 void CMergeDoc::OnUpdateFileRecompareAsXML(CCmdUI *pCmdUI)
3515 pCmdUI->Enable(m_pInfoUnpacker->m_PluginOrPredifferMode != PLUGIN_MODE::PLUGIN_BUILTIN_XML);
3518 void CMergeDoc::OnFileRecompareAs(UINT nID)
3520 DWORD dwFlags[3] = { 0 };
3521 FileLocation fileloc[3];
3522 for (int pane = 0; pane < m_nBuffers; pane++)
3524 fileloc[pane].setPath(m_filePaths[pane]);
3525 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3527 if (m_pEncodingErrorBar!=nullptr && m_pEncodingErrorBar->IsWindowVisible())
3528 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3529 GetMainFrame()->ShowMergeDoc(nID, m_pDirDoc, m_nBuffers, fileloc, dwFlags, m_strDesc);
3530 GetParentFrame()->ShowWindow(SW_RESTORE);
3531 GetParentFrame()->DestroyWindow();
3534 // Return file extension either from file name
3535 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3538 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3543 * @brief Generate report from file compare results.
3545 bool CMergeDoc::GenerateReport(const String& sFileName) const
3547 // calculate HTML font size
3550 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3551 m_pView[0][0]->GetFont(lf);
3552 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3554 // create HTML report
3556 if (!file.Open(sFileName, _T("wt")))
3558 String errMsg = GetSysError(GetLastError());
3559 String msg = strutils::format_string1(
3560 _("Error creating the report:\n%1"), errMsg);
3561 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3565 file.SetCodepage(ucr::CP_UTF_8);
3567 CString headerText =
3568 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3569 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3572 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3573 _T("<title>WinMerge File Compare Report</title>\n")
3574 _T("<style type=\"text/css\">\n")
3576 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3577 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3578 _T("tr { vertical-align: top; }\n")
3579 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3585 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3589 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3590 file.WriteString(header);
3593 // If archive, use archive path + folder + filename inside archive
3594 // If desc text given, use it
3595 PathContext paths = m_filePaths;
3596 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3598 for (int i = 0; i < paths.GetSize(); i++)
3599 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3603 for (int i = 0; i < paths.GetSize(); i++)
3605 if (!m_strDesc[i].empty())
3606 paths[i] = m_strDesc[i];
3612 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3614 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3615 (double)100 / m_nBuffers);
3616 file.WriteString(data);
3617 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3618 file.WriteString(_T("</th>\n"));
3625 // write the body of the report
3627 int nLineCount[3] = {0};
3629 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3630 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3634 file.WriteString(_T("<tr>\n"));
3635 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3637 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3639 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3643 if (idx[nBuffer] < nLineCount[nBuffer])
3646 int iVisibleLineNumber = 0;
3647 String tdtag = _T("<td class=\"ln\">");
3648 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3649 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3651 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3654 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3655 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3658 if (iVisibleLineNumber > 0)
3660 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3661 iVisibleLineNumber = 0;
3664 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3666 if (iVisibleLineNumber > 0)
3667 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3669 tdtag += _T("</td>");
3670 file.WriteString(tdtag);
3672 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3676 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3677 file.WriteString(_T("\n"));
3679 file.WriteString(_T("</tr>\n"));
3681 bool bBorderLine = false;
3682 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3684 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3690 file.WriteString(_T("<tr height=1>"));
3691 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3693 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3694 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3696 file.WriteString(_T("<td></td><td></td>"));
3698 file.WriteString(_T("</tr>\n"));
3701 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3716 * @brief Generate report from file compare results.
3718 void CMergeDoc::OnToolsGenerateReport()
3723 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3726 if (GenerateReport(s.c_str()))
3727 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3731 * @brief Generate patch from files selected.
3733 * Creates a patch from selected files in active directory compare, or
3734 * active file compare. Files in file compare must be saved before
3737 void CMergeDoc::OnToolsGeneratePatch()
3739 // If there are changes in files, tell user to save them first
3742 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3747 patcher.AddFiles(m_filePaths.GetLeft(),
3748 m_filePaths.GetRight());
3749 patcher.CreatePatch();
3753 * @brief Add synchronization point
3755 void CMergeDoc::AddSyncPoint()
3758 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3760 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3761 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3764 // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3765 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3766 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3768 LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3772 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3773 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3774 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3776 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3777 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3779 m_bHasSyncPoints = true;
3781 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3783 FlushAndRescan(true);
3787 * @brief Delete a synchronization point
3789 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3791 const auto syncpoints = GetSyncPointList();
3792 for (auto syncpnt : syncpoints)
3794 if (syncpnt[pane] == nLine)
3796 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3797 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3801 if (syncpoints.size() == 1)
3802 m_bHasSyncPoints = false;
3805 FlushAndRescan(true);
3810 * @brief Clear Synchronization points
3812 void CMergeDoc::ClearSyncPoints()
3814 if (!m_bHasSyncPoints)
3817 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3819 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3820 for (int nLine = 0; nLine < nLineCount; ++nLine)
3822 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3823 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3827 m_bHasSyncPoints = false;
3829 FlushAndRescan(true);
3832 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3834 std::vector<std::vector<int> > list;
3835 if (!m_bHasSyncPoints)
3837 int idx[3] = {-1, -1, -1};
3838 std::vector<int> points(m_nBuffers);
3839 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3840 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3841 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3843 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3844 for (int nLine = 0; nLine < nLineCount; ++nLine)
3846 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3849 if (static_cast<int>(list.size()) <= idx[nBuffer])
3850 list.push_back(points);
3851 list[idx[nBuffer]][nBuffer] = nLine;