1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
10 * @brief Implementation file for CMergeDoc
17 #include <Poco/Timestamp.h>
18 #include "UnicodeString.h"
21 #include "DiffTextBuffer.h"
22 #include "Environment.h"
23 #include "MovedLines.h"
24 #include "MergeEditView.h"
25 #include "MergeEditFrm.h"
28 #include "FileTransform.h"
32 #include "OptionsDef.h"
33 #include "DiffFileInfo.h"
34 #include "SaveClosingDlg.h"
35 #include "OpenTableDlg.h"
38 #include "OptionsMgr.h"
39 #include "OptionsDiffOptions.h"
40 #include "MergeLineFlags.h"
41 #include "FileOrFolderSelect.h"
42 #include "LineFiltersList.h"
43 #include "SubstitutionFiltersList.h"
45 #include "codepage_detect.h"
46 #include "SelectPluginDlg.h"
47 #include "EncodingErrorBar.h"
48 #include "MergeCmdLineInfo.h"
50 #include "Constants.h"
51 #include "Merge7zFormatMergePluginImpl.h"
53 #include "PatchTool.h"
56 #include "stringdiffs.h"
64 int CMergeDoc::m_nBuffersTemp = 2;
66 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine = 0, int nLines = -1);
68 /////////////////////////////////////////////////////////////////////////////
71 IMPLEMENT_DYNCREATE(CMergeDoc, CDocument)
73 BEGIN_MESSAGE_MAP(CMergeDoc, CDocument)
74 //{{AFX_MSG_MAP(CMergeDoc)
75 ON_COMMAND(ID_FILE_SAVE, OnFileSave)
76 ON_COMMAND(ID_FILE_SAVE_LEFT, OnFileSaveLeft)
77 ON_COMMAND(ID_FILE_SAVE_MIDDLE, OnFileSaveMiddle)
78 ON_COMMAND(ID_FILE_SAVE_RIGHT, OnFileSaveRight)
79 ON_COMMAND(ID_FILE_SAVEAS_LEFT, OnFileSaveAsLeft)
80 ON_UPDATE_COMMAND_UI(ID_FILE_SAVEAS_MIDDLE, OnUpdateFileSaveAsMiddle)
81 ON_COMMAND(ID_FILE_SAVEAS_MIDDLE, OnFileSaveAsMiddle)
82 ON_COMMAND(ID_FILE_SAVEAS_RIGHT, OnFileSaveAsRight)
83 ON_UPDATE_COMMAND_UI(ID_STATUS_DIFFNUM, OnUpdateStatusNum)
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_OPEN_WITH_UNPACKER, OnOpenWithUnpacker)
91 ON_COMMAND(ID_APPLY_PREDIFFER, OnApplyPrediffer)
92 ON_COMMAND_RANGE(ID_NO_PREDIFFER, ID_NO_PREDIFFER, OnPrediffer)
93 ON_COMMAND_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnPrediffer)
94 ON_UPDATE_COMMAND_UI(ID_NO_PREDIFFER, OnUpdatePrediffer)
95 ON_UPDATE_COMMAND_UI_RANGE(ID_PREDIFFERS_FIRST, ID_PREDIFFERS_LAST, OnUpdatePrediffer)
96 ON_BN_CLICKED(IDC_FILEENCODING, OnBnClickedFileEncoding)
97 ON_BN_CLICKED(IDC_PLUGIN, OnBnClickedPlugin)
98 ON_BN_CLICKED(IDC_HEXVIEW, OnBnClickedHexView)
99 ON_COMMAND(IDOK, OnOK)
100 ON_COMMAND(ID_MERGE_COMPARE_TEXT, OnFileRecompareAsText)
101 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TEXT, OnUpdateFileRecompareAsText)
102 ON_COMMAND(ID_MERGE_COMPARE_TABLE, OnFileRecompareAsTable)
103 ON_UPDATE_COMMAND_UI(ID_MERGE_COMPARE_TABLE, OnUpdateFileRecompareAsTable)
104 ON_COMMAND_RANGE(ID_MERGE_COMPARE_HEX, ID_MERGE_COMPARE_IMAGE, OnFileRecompareAs)
105 ON_COMMAND_RANGE(ID_UNPACKERS_FIRST, ID_UNPACKERS_LAST, OnFileRecompareAs)
106 ON_COMMAND(ID_SWAPPANES_SWAP12, (OnViewSwapPanes<0, 1>))
107 ON_COMMAND(ID_SWAPPANES_SWAP23, (OnViewSwapPanes<1, 2>))
108 ON_COMMAND(ID_SWAPPANES_SWAP13, (OnViewSwapPanes<0, 2>))
109 ON_UPDATE_COMMAND_UI_RANGE(ID_SWAPPANES_SWAP23, ID_SWAPPANES_SWAP13, OnUpdateSwapContext)
113 /////////////////////////////////////////////////////////////////////////////
114 // CMergeDoc construction/destruction
117 * @brief Constructor.
119 CMergeDoc::CMergeDoc()
120 : m_bEnableRescan(true)
122 , m_CurWordDiff{ -1, static_cast<size_t>(-1), -1 }
125 , m_pEncodingErrorBar(nullptr)
126 , m_bHasSyncPoints(false)
127 , m_bAutoMerged(false)
130 , m_bAutomaticRescan(false)
131 , m_CurrentPredifferID(0)
133 DIFFOPTIONS options = {0};
135 m_nBuffers = m_nBuffersTemp;
136 m_filePaths.SetSize(m_nBuffers);
138 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
140 m_ptBuf[nBuffer].reset(new CDiffTextBuffer(this, nBuffer));
141 m_pSaveFileInfo[nBuffer].reset(new DiffFileInfo());
142 m_pRescanFileInfo[nBuffer].reset(new DiffFileInfo());
143 m_nBufferType[nBuffer] = BUFFERTYPE::NORMAL;
144 m_bEditAfterRescan[nBuffer] = false;
147 m_bEnableRescan = true;
148 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
150 // COleDateTime m_LastRescan
151 curUndo = undoTgt.begin();
152 m_nDiffContext = GetOptionsMgr()->GetInt(OPT_DIFF_CONTEXT);
153 m_bInvertDiffContext = GetOptionsMgr()->GetBool(OPT_INVERT_DIFF_CONTEXT);
155 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
156 Options::DiffOptions::Load(GetOptionsMgr(), options);
158 m_diffWrapper.SetOptions(&options);
159 m_diffWrapper.SetPrediffer(nullptr);
165 * Informs associated dirdoc that mergedoc is closing.
167 CMergeDoc::~CMergeDoc()
169 if (m_pDirDoc != nullptr)
171 m_pDirDoc->MergeDocClosing(this);
177 * @brief Deleted data associated with doc before closing.
179 void CMergeDoc::DeleteContents ()
181 CDocument::DeleteContents ();
182 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
184 m_ptBuf[nBuffer]->FreeAll ();
185 m_tempFiles[nBuffer].Delete();
190 * @brief Called when new document is created.
192 * Initialises buffers.
194 BOOL CMergeDoc::OnNewDocument()
196 if (!CDocument::OnNewDocument())
199 SetTitle(_("File Comparison").c_str());
201 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
202 m_ptBuf[nBuffer]->InitNew ();
207 * @brief Return active merge edit view (or left one if neither active)
209 CMergeEditView * CMergeDoc::GetActiveMergeView()
211 CView * pActiveView = GetParentFrame()->GetActiveView();
212 CMergeEditView * pMergeEditView = dynamic_cast<CMergeEditView *>(pActiveView);
213 if (pMergeEditView == nullptr)
214 pMergeEditView = GetView(0, 0); // default to left view (in case some location or detail view active)
215 return pMergeEditView;
218 CMergeEditView * CMergeDoc::GetActiveMergeGroupView(int nBuffer)
220 return m_pView[GetActiveMergeView()->m_nThisGroup][nBuffer];
223 void CMergeDoc::SetUnpacker(const PackingInfo * infoNewHandler)
225 if (infoNewHandler != nullptr)
227 m_infoUnpacker = *infoNewHandler;
231 void CMergeDoc::SetPrediffer(const PrediffingInfo * infoPrediffer)
233 m_diffWrapper.SetPrediffer(infoPrediffer);
236 void CMergeDoc::GetPrediffer(PrediffingInfo * infoPrediffer)
238 m_diffWrapper.GetPrediffer(infoPrediffer);
241 const PrediffingInfo* CMergeDoc::GetPrediffer() const
243 static PrediffingInfo infoPrediffer;
244 m_diffWrapper.GetPrediffer(&infoPrediffer);
245 return &infoPrediffer;
248 /////////////////////////////////////////////////////////////////////////////
249 // CMergeDoc serialization
251 void CMergeDoc::Serialize(CArchive& ar)
253 ASSERT(false); // we do not use CDocument serialization
256 /////////////////////////////////////////////////////////////////////////////
257 // CMergeDoc commands
260 * @brief Save an editor text buffer to a file for prediffing (make UCS-2LE if appropriate)
263 * original file is Ansi :
264 * buffer -> save as Ansi -> Ansi plugins -> diffutils
265 * original file is Unicode (UCS2-LE, UCS2-BE, UTF-8) :
266 * buffer -> save as UTF-8 -> Unicode plugins -> convert to UTF-8 -> diffutils
267 * (the plugins are optional, not the conversion)
268 * @todo Show SaveToFile() errors?
270 static void SaveBuffForDiff(CDiffTextBuffer & buf, const String& filepath, int nStartLine, int nLines)
272 // and we don't repack the file
273 PackingInfo tempPacker(false);
275 // write buffer out to temporary file
277 int retVal = buf.SaveToFile(filepath, true, sError, tempPacker,
278 CRLFSTYLE::AUTOMATIC, false, nStartLine, nLines);
282 * @brief Save files to temp files & compare again.
284 * @param bBinary [in,out] [in] If true, compare two binary files
285 * [out] If true binary file was detected.
286 * @param bIdentical [out] If true files were identical
287 * @param bForced [in] If true, suppressing is ignored and rescan
289 * @return Tells if rescan was successfully, was suppressed, or
291 * If this code is OK, Rescan has detached the views temporarily
292 * (positions of cursors have been lost)
293 * @note Rescan() ALWAYS compares temp files. Actual user files are not
294 * touched by Rescan().
295 * @sa CDiffWrapper::RunFileDiff()
297 int CMergeDoc::Rescan(bool &bBinary, IDENTLEVEL &identical,
298 bool bForced /* =false */)
300 DIFFOPTIONS diffOptions = {0};
301 DiffFileInfo fileInfo;
302 bool diffSuccess = false;
303 int nResult = RESCAN_OK;
304 FileChange Changed[3] = {FileChange::NoChange, FileChange::NoChange, FileChange::NoChange};
309 if (!m_bEnableRescan)
310 return RESCAN_SUPPRESSED;
313 ClearWordDiffCache();
315 if (GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED))
317 m_diffWrapper.SetFilterList(theApp.m_pLineFilters->GetAsString());
321 m_diffWrapper.SetFilterList(_T(""));
324 if (theApp.m_pSubstitutionFiltersList && theApp.m_pSubstitutionFiltersList->GetEnabled())
326 m_diffWrapper.SetSubstitutionList(theApp.m_pSubstitutionFiltersList->MakeSubstitutionList());
330 m_diffWrapper.SetSubstitutionList(nullptr);
333 if (GetView(0, 0)->m_CurSourceDef->type != 0)
334 m_diffWrapper.SetFilterCommentsSourceDef(GetView(0, 0)->m_CurSourceDef);
336 m_diffWrapper.SetFilterCommentsSourceDef(GetFileExt(m_filePaths[0].c_str(), m_strDesc[0].c_str()));
338 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
340 // Check if files have been modified since last rescan
341 // Ignore checking in case of scratchpads (empty filenames)
342 if (!m_filePaths[nBuffer].empty())
344 Changed[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(),
345 fileInfo, false, nBuffer);
348 m_LastRescan = COleDateTime::GetCurrentTime();
350 LPCTSTR tnames[] = {_T("t0_wmdoc"), _T("t1_wmdoc"), _T("t2_wmdoc")};
351 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
353 if (Changed[nBuffer] == FileChange::Removed)
355 String msg = strutils::format_string1(_("The file\n%1\nhas disappeared. Please save a copy of the file to continue."), m_filePaths[nBuffer]);
356 ShowMessageBox(msg, MB_ICONWARNING);
357 bool bSaveResult = false;
358 bool ok = DoSaveAs(m_filePaths[nBuffer].c_str(), bSaveResult, nBuffer);
359 if (!ok || !bSaveResult)
361 return RESCAN_FILE_ERR;
365 String temp = m_tempFiles[nBuffer].GetPath();
367 temp = m_tempFiles[nBuffer].Create(tnames[nBuffer]);
369 return RESCAN_TEMP_ERR;
374 String tempPath = env::GetTemporaryPath();
376 // Set up DiffWrapper
377 m_diffWrapper.GetOptions(&diffOptions);
382 m_CurWordDiff = { -1, static_cast<size_t>(-1), -1 };
383 // Clear moved lines lists
384 if (m_diffWrapper.GetDetectMovedBlocks())
386 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
387 m_diffWrapper.GetMovedLines(nBuffer)->Clear();
390 // Set paths for diffing and run diff
391 m_diffWrapper.EnablePlugins(GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED));
393 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath()), true);
395 m_diffWrapper.SetPaths(PathContext(m_tempFiles[0].GetPath(), m_tempFiles[1].GetPath(), m_tempFiles[2].GetPath()), true);
396 m_diffWrapper.SetCompareFiles(m_filePaths);
400 if (!HasSyncPoints())
402 // Save text buffer to file
403 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
405 m_ptBuf[nBuffer]->SetTempPath(tempPath);
406 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath());
409 m_diffWrapper.SetCreateDiffList(&m_diffList);
410 diffSuccess = m_diffWrapper.RunFileDiff();
413 m_diffWrapper.GetDiffStatus(&status);
414 if (bBinary) // believe caller if we were told these are binaries
415 status.bBinaries = true;
419 const std::vector<std::vector<int> > syncpoints = GetSyncPointList();
420 int nStartLine[3] = {0};
421 int nLines[3], nRealLine[3];
422 for (size_t i = 0; i <= syncpoints.size(); ++i)
424 // Save text buffer to file
425 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
427 nLines[nBuffer] = (i >= syncpoints.size()) ? -1 : syncpoints[i][nBuffer] - nStartLine[nBuffer];
428 m_ptBuf[nBuffer]->SetTempPath(tempPath);
429 SaveBuffForDiff(*m_ptBuf[nBuffer], m_tempFiles[nBuffer].GetPath(),
430 nStartLine[nBuffer], nLines[nBuffer]);
434 m_diffWrapper.SetCreateDiffList(&templist);
435 diffSuccess = m_diffWrapper.RunFileDiff();
436 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
437 nRealLine[nBuffer] = m_ptBuf[nBuffer]->ComputeRealLine(nStartLine[nBuffer]);
439 // Correct the comparison results made by diffutils if the first file separated by the sync point is an empty file.
440 if (i == 0 && templist.GetSize() > 0)
441 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
442 if (nStartLine[nBuffer] == 0)
444 bool isEmptyFile = true;
445 for (int j = 0; j < nLines[nBuffer]; j++)
447 if (!(m_ptBuf[nBuffer]->GetLineFlags(nStartLine[nBuffer] + j) & LF_GHOST))
456 templist.GetDiff(0, di);
457 if (di.begin[nBuffer] == 0 && di.end[nBuffer] == 0)
459 di.end[nBuffer] = -1;
460 templist.SetDiff(0, di);
465 m_diffList.AppendDiffList(templist, nRealLine);
466 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
467 nStartLine[nBuffer] += nLines[nBuffer];
470 DIFFSTATUS status_part;
471 m_diffWrapper.GetDiffStatus(&status_part);
472 if (bBinary) // believe caller if we were told these are binaries
473 status.bBinaries = true;
474 status.MergeStatus(status_part);
476 m_diffWrapper.SetCreateDiffList(&m_diffList);
479 // If one file has EOL before EOF and other not...
480 if (std::count(status.bMissingNL, status.bMissingNL + m_nBuffers, status.bMissingNL[0]) < m_nBuffers)
482 // ..last DIFFRANGE of file which has EOL must be
483 // fixed to contain last line too
484 int lineCount[3] = { 0,0,0 };
485 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
486 lineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
487 m_diffWrapper.FixLastDiffRange(m_nBuffers, lineCount, status.bMissingNL, diffOptions.bIgnoreBlankLines);
490 // set identical/diff result as recorded by diffutils
491 identical = status.Identical;
493 // Determine errors and binary file compares
495 nResult = RESCAN_FILE_ERR;
496 else if (status.bBinaries)
502 // Now update views and buffers for ghost lines
504 // Prevent displaying views during this update
505 // BTW, this solves the problem of double asserts
506 // (during the display of an assert message box, a second assert in one of the
507 // display functions happens, and hides the first assert)
508 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
510 // Remove blank lines and clear winmerge flags
511 // this operation does not change the modified flag
512 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
513 m_ptBuf[nBuffer]->prepareForRescan();
515 // Divide diff blocks to match lines.
516 if (GetOptionsMgr()->GetBool(OPT_CMP_MATCH_SIMILAR_LINES) && m_nBuffers < 3)
519 // Analyse diff-list (updating real line-numbers)
520 // this operation does not change the modified flag
523 // Hide identical lines if diff-context is not 'All'
526 // Apply flags to lines that are trivial
527 PrediffingInfo infoPrediffer;
528 GetPrediffer(&infoPrediffer);
529 if (!infoPrediffer.GetPluginPipeline().empty())
532 // Apply flags to lines that moved, to differentiate from appeared/disappeared lines
533 if (m_diffWrapper.GetDetectMovedBlocks())
536 // After PrimeTextBuffers() we know amount of real diffs
537 // (m_nDiffs) and trivial diffs (m_nTrivialDiffs)
539 // Identical files are also updated
540 if (!m_diffList.HasSignificantDiffs())
541 identical = IDENTLEVEL::ALL;
543 ForEachView([](auto& pView) {
544 // just apply some options to the views
545 pView->PrimeListWithFile();
546 // Now buffers data are valid
547 pView->ReAttachToBuffer();
549 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
551 m_bEditAfterRescan[nBuffer] = false;
555 if (!GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE) &&
556 identical == IDENTLEVEL::ALL &&
557 std::any_of(m_ptBuf, m_ptBuf + m_nBuffers,
558 [&](std::unique_ptr<CDiffTextBuffer>& buf) { return buf->getEncoding() != m_ptBuf[0]->getEncoding(); }))
559 identical = IDENTLEVEL::NONE;
561 GetParentFrame()->SetLastCompareResult(identical != IDENTLEVEL::ALL ? 1 : 0);
566 void CMergeDoc::CheckFileChanged(void)
569 DiffFileInfo fileInfo;
570 FileChange FileChange[3];
572 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
574 FileChange[nBuffer] = IsFileChangedOnDisk(m_filePaths[nBuffer].c_str(), fileInfo,
577 m_pRescanFileInfo[nBuffer]->Update((LPCTSTR)m_filePaths[nBuffer].c_str());
580 bool bDoReload = false;
581 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
583 if (FileChange[nBuffer] == FileChange::Changed)
585 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]);
586 if (ShowMessageBox(msg, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_FILECHANGED_RESCAN) == IDYES)
593 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
595 if (FileChange[nBuffer] == FileChange::Changed)
597 CPoint pt = GetView(0, nBuffer)->GetCursorPos();
598 ChangeFile(nBuffer, m_filePaths[nBuffer], pt.y);
604 /** @brief Apply flags to lines that are trivial */
605 void CMergeDoc::FlagTrivialLines(void)
607 for (int i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
609 if ((m_ptBuf[0]->GetLineFlags(i) & LF_NONTRIVIAL_DIFF) == 0)
612 for (int file = 0; file < m_nBuffers; ++file)
614 const TCHAR *p = m_ptBuf[file]->GetLineChars(i);
615 str[file] = p ? p : _T("");
618 if (std::count(str + 1, str + m_nBuffers, str[0]) != m_nBuffers - 1)
620 DIFFOPTIONS diffOptions = {0};
621 m_diffWrapper.GetOptions(&diffOptions);
623 // Make the call to stringdiffs, which does all the hard & tedious computations
624 std::vector<strdiff::wdiff> worddiffs = strdiff::ComputeWordDiffs(m_nBuffers, str,
625 !diffOptions.bIgnoreCase,
626 !diffOptions.bIgnoreEol,
627 diffOptions.nIgnoreWhitespace,
628 GetBreakType(), // whitespace only or include punctuation
629 GetByteColoringOption());
630 if (!worddiffs.empty())
632 for (int file = 0; file < m_nBuffers; ++file)
633 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
640 /** @brief Adjust all different lines that were detected as actually matching moved lines */
641 void CMergeDoc::FlagMovedLines(void)
644 MovedLines *pMovedLines;
646 pMovedLines = m_diffWrapper.GetMovedLines(0);
647 for (i = 0; i < m_ptBuf[0]->GetLineCount(); ++i)
649 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
652 TRACE(_T("%d->%d\n"), i, j);
654 // We only flag lines that are already marked as being different
655 int apparent = m_ptBuf[0]->ComputeApparentLine(i);
656 if (m_ptBuf[0]->FlagIsSet(apparent, LF_DIFF))
658 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
659 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
661 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
662 if (m_ptBuf[0]->FlagIsSet(apparentJ, LF_GHOST))
663 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
669 pMovedLines = m_diffWrapper.GetMovedLines(1);
670 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
672 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
675 TRACE(_T("%d->%d\n"), i, j);
677 // We only flag lines that are already marked as being different
678 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
679 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
681 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
682 if (m_ptBuf[0]->FlagIsSet(apparent, LF_GHOST))
684 int apparentJ = m_ptBuf[0]->ComputeApparentLine(j);
685 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
686 m_ptBuf[0]->SetLineFlag(apparent, LF_MOVED, true, false, false);
695 pMovedLines = m_diffWrapper.GetMovedLines(1);
696 for (i=0; i<m_ptBuf[1]->GetLineCount(); ++i)
698 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::RIGHT);
701 TRACE(_T("%d->%d\n"), i, j);
703 // We only flag lines that are already marked as being different
704 int apparent = m_ptBuf[1]->ComputeApparentLine(i);
705 if (m_ptBuf[1]->FlagIsSet(apparent, LF_DIFF))
707 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
708 if (m_ptBuf[2]->FlagIsSet(apparent, LF_GHOST))
710 int apparentJ = m_ptBuf[2]->ComputeApparentLine(j);
711 if (m_ptBuf[1]->FlagIsSet(apparentJ, LF_GHOST))
712 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
718 pMovedLines = m_diffWrapper.GetMovedLines(2);
719 for (i=0; i<m_ptBuf[2]->GetLineCount(); ++i)
721 int j = pMovedLines->LineInBlock(i, MovedLines::SIDE::LEFT);
724 TRACE(_T("%d->%d\n"), i, j);
726 // We only flag lines that are already marked as being different
727 int apparent = m_ptBuf[2]->ComputeApparentLine(i);
728 if (m_ptBuf[2]->FlagIsSet(apparent, LF_DIFF))
730 m_ptBuf[2]->SetLineFlag(apparent, LF_MOVED, true, false, false);
731 if (m_ptBuf[1]->FlagIsSet(apparent, LF_GHOST))
733 int apparentJ = m_ptBuf[1]->ComputeApparentLine(j);
734 if (m_ptBuf[2]->FlagIsSet(apparentJ, LF_GHOST))
735 m_ptBuf[1]->SetLineFlag(apparent, LF_MOVED, true, false, false);
741 // todo: Need to record actual moved information
744 int CMergeDoc::ShowMessageBox(const String& sText, unsigned nType, unsigned nIDHelp)
746 if (m_pView[0][0] && m_pView[0][0]->IsTextBufferInitialized() && !GetParentFrame()->IsActivated())
748 GetParentFrame()->InitialUpdateFrame(this, true);
749 GetParentFrame()->SendMessageToDescendants(WM_IDLEUPDATECMDUI, static_cast<WPARAM>(true), 0, true, true);
751 return AfxMessageBox(sText.c_str(), nType, nIDHelp);
755 * @brief Prints (error) message by rescan status.
757 * @param nRescanResult [in] Resultcocode from rescan().
758 * @param bIdentical [in] Were files identical?.
759 * @sa CMergeDoc::Rescan()
761 void CMergeDoc::ShowRescanError(int nRescanResult, IDENTLEVEL identical)
763 // Rescan was suppressed, there is no sensible status
764 if (nRescanResult == RESCAN_SUPPRESSED)
769 if (nRescanResult == RESCAN_FILE_ERR)
771 s = _("An error occurred while comparing the files.");
773 ShowMessageBox(s, MB_ICONSTOP);
777 if (nRescanResult == RESCAN_TEMP_ERR)
779 s = _("Temporary files could not be created. Check your temporary path settings.");
781 ShowMessageBox(s, MB_ICONSTOP);
785 // Files are not binaries, but they are identical
786 if (identical != IDENTLEVEL::NONE)
788 CMergeFrameCommon::ShowIdenticalMessage(m_filePaths, identical == IDENTLEVEL::ALL,
789 [this](LPCTSTR msg, UINT flags, UINT id) -> int { return ShowMessageBox(msg, flags, id); });
793 bool CMergeDoc::Undo()
799 * @brief An instance of RescanSuppress prevents rescan during its lifetime
800 * (or until its Clear method is called, which ends its effect).
805 explicit RescanSuppress(CMergeDoc & doc) : m_doc(doc)
808 m_bPrev = doc.m_bEnableRescan;
809 m_doc.m_bEnableRescan = false;
816 m_doc.m_bEnableRescan = m_bPrev;
830 * @brief Copy all diffs from one side to side.
831 * @param [in] srcPane Source side from which diff is copied
832 * @param [in] dstPane Destination side
834 void CMergeDoc::CopyAllList(int srcPane, int dstPane)
836 CopyMultipleList(srcPane, dstPane, 0, m_diffList.GetSize() - 1);
840 * @brief Copy range of diffs from one side to side.
841 * This function copies given range of differences from side to another.
842 * Ignored differences are skipped, and not copied.
843 * @param [in] srcPane Source side from which diff is copied
844 * @param [in] dstPane Destination side
845 * @param [in] firstDiff First diff copied (0-based index)
846 * @param [in] lastDiff Last diff copied (0-based index)
848 void CMergeDoc::CopyMultipleList(int srcPane, int dstPane, int firstDiff, int lastDiff, int firstWordDiff, int lastWordDiff)
851 if (firstDiff > lastDiff)
852 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff > lastDiff)!");
854 _RPTF0(_CRT_ERROR, "Invalid diff range (firstDiff < 0)!");
855 if (lastDiff > m_diffList.GetSize() - 1)
856 _RPTF0(_CRT_ERROR, "Invalid diff range (lastDiff < diffcount)!");
859 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
860 firstDiff = max(0, firstDiff);
861 if (firstDiff > lastDiff)
864 RescanSuppress suppressRescan(*this);
866 // Note we don't care about m_nDiffs count to become zero,
867 // because we don't rescan() so it does not change
869 SetCurrentDiff(lastDiff);
871 bool bGroupWithPrevious = false;
872 if (firstWordDiff <= 0 && lastWordDiff == -1)
874 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
875 return; // sync failure
879 if (!WordListCopy(srcPane, dstPane, lastDiff,
880 (firstDiff == lastDiff) ? firstWordDiff : 0, lastWordDiff, nullptr, bGroupWithPrevious, true))
881 return; // sync failure
884 SetEditedAfterRescan(dstPane);
886 int nGroup = GetActiveMergeView()->m_nThisGroup;
887 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
888 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
889 CPoint currentPosSrc = pViewSrc->GetCursorPos();
891 CPoint currentPosDst = pViewDst->GetCursorPos();
895 pViewDst->SetCursorPos(pt);
896 pViewDst->SetNewSelection(pt, pt, false);
897 pViewDst->SetNewAnchor(pt);
899 // copy from bottom up is more efficient
900 for (int i = lastDiff - 1; i >= firstDiff; --i)
902 if (m_diffList.IsDiffSignificant(i))
905 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
906 if (currentPosDst.y > pdi->dend)
908 if (pdi->blank[dstPane] >= 0)
909 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
910 else if (pdi->blank[srcPane] >= 0)
911 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
913 // Group merge with previous (merge undo data to one action)
914 bGroupWithPrevious = true;
915 if (i > firstDiff || firstWordDiff <= 0)
917 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
918 break; // sync failure
922 if (!WordListCopy(srcPane, dstPane, firstDiff, firstWordDiff, -1, nullptr, bGroupWithPrevious, false))
923 break; // sync failure
928 ForEachView(dstPane, [currentPosDst](auto& pView) {
929 pView->SetCursorPos(currentPosDst);
930 pView->SetNewSelection(currentPosDst, currentPosDst, false);
931 pView->SetNewAnchor(currentPosDst);
934 suppressRescan.Clear(); // done suppress Rescan
938 void CMergeDoc::CopyMultiplePartialList(int srcPane, int dstPane, int firstDiff, int lastDiff,
939 int firstLineDiff, int lastLineDiff)
941 lastDiff = min(m_diffList.GetSize() - 1, lastDiff);
942 firstDiff = max(0, firstDiff);
943 if (firstDiff > lastDiff)
946 RescanSuppress suppressRescan(*this);
948 bool bGroupWithPrevious = false;
949 if (firstLineDiff <= 0 && lastLineDiff == -1)
951 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, true))
952 return; // sync failure
956 if (!PartialListCopy(srcPane, dstPane, lastDiff,
957 (firstDiff == lastDiff) ? firstLineDiff : 0, lastLineDiff, bGroupWithPrevious, true))
958 return; // sync failure
962 SetEditedAfterRescan(dstPane);
964 int nGroup = GetActiveMergeView()->m_nThisGroup;
965 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
966 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
967 CPoint currentPosSrc = pViewSrc->GetCursorPos();
969 CPoint currentPosDst = pViewDst->GetCursorPos();
973 pViewDst->SetCursorPos(pt);
974 pViewDst->SetNewSelection(pt, pt, false);
975 pViewDst->SetNewAnchor(pt);
977 // copy from bottom up is more efficient
978 for (int i = lastDiff - 1; i >= firstDiff; --i)
980 if (m_diffList.IsDiffSignificant(i))
983 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
984 if (currentPosDst.y > pdi->dend)
986 if (pdi->blank[dstPane] >= 0)
987 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
988 else if (pdi->blank[srcPane] >= 0)
989 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
991 // Group merge with previous (merge undo data to one action)
992 bGroupWithPrevious = true;
993 if (i > firstDiff || firstLineDiff <= 0)
995 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
996 break; // sync failure
1000 if (!PartialListCopy(srcPane, dstPane, firstDiff, firstLineDiff, -1, bGroupWithPrevious, false))
1001 break; // sync failure
1006 ForEachView(dstPane, [currentPosDst](auto& pView) {
1007 pView->SetCursorPos(currentPosDst);
1008 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1009 pView->SetNewAnchor(currentPosDst);
1012 suppressRescan.Clear(); // done suppress Rescan
1016 enum MergeResult { NoMergeNeeded, Merged, Conflict };
1018 template<class Type>
1019 static std::pair<MergeResult, Type> DoMergeValue(Type left, Type middle, Type right, int dstPane)
1021 bool equal_all = middle == left && middle == right && left == right;
1023 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1024 bool conflict = middle != left && middle != right && left != right;
1026 return std::pair<MergeResult, Type>(Conflict, left);
1031 return std::pair<MergeResult, Type>(Merged, middle);
1035 return std::pair<MergeResult, Type>(Merged, right);
1037 return std::pair<MergeResult, Type>(Merged, left);
1041 return std::pair<MergeResult, Type>(Merged, middle);
1044 return std::pair<MergeResult, Type>(NoMergeNeeded, left);
1048 * @brief Do auto-merge.
1049 * @param [in] dstPane Destination side
1051 void CMergeDoc::DoAutoMerge(int dstPane)
1053 const int lastDiff = m_diffList.GetSize() - 1;
1054 const int firstDiff = 0;
1055 bool bGroupWithPrevious = false;
1056 int autoMergedCount = 0;
1057 int unresolvedConflictCount = 0;
1059 std::pair<MergeResult, FileTextEncoding> mergedEncoding =
1060 DoMergeValue(m_ptBuf[0]->getEncoding(), m_ptBuf[1]->getEncoding(), m_ptBuf[2]->getEncoding(), dstPane);
1061 if (mergedEncoding.first == Merged)
1063 ShowMessageBox(_("The change of codepage has been merged."), MB_ICONINFORMATION);
1064 m_ptBuf[dstPane]->setEncoding(mergedEncoding.second);
1066 else if (mergedEncoding.first == Conflict)
1067 ShowMessageBox(_("The changes of codepage are conflicting."), MB_ICONINFORMATION);
1069 std::pair<MergeResult, CRLFSTYLE> mergedEOLStyle =
1070 DoMergeValue(m_ptBuf[0]->GetCRLFMode(), m_ptBuf[1]->GetCRLFMode(), m_ptBuf[2]->GetCRLFMode(), dstPane);
1071 if (mergedEOLStyle.first == Merged)
1073 ShowMessageBox(_("The change of EOL has been merged."), MB_ICONINFORMATION);
1074 m_ptBuf[dstPane]->SetCRLFMode(mergedEOLStyle.second);
1076 else if (mergedEOLStyle.first == Conflict)
1077 ShowMessageBox(_("The changes of EOL are conflicting."), MB_ICONINFORMATION);
1079 RescanSuppress suppressRescan(*this);
1081 // Note we don't care about m_nDiffs count to become zero,
1082 // because we don't rescan() so it does not change
1084 SetCurrentDiff(lastDiff);
1086 SetEditedAfterRescan(dstPane);
1088 int nGroup = GetActiveMergeView()->m_nThisGroup;
1089 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1090 CPoint currentPosDst = pViewDst->GetCursorPos();
1091 currentPosDst.x = 0;
1094 pViewDst->SetCursorPos(pt);
1095 pViewDst->SetNewSelection(pt, pt, false);
1096 pViewDst->SetNewAnchor(pt);
1098 // copy from bottom up is more efficient
1099 for (int i = lastDiff; i >= firstDiff; --i)
1101 const DIFFRANGE *pdi = m_diffList.DiffRangeAt(i);
1102 const int srcPane = m_diffList.GetMergeableSrcIndex(i, dstPane);
1106 if (currentPosDst.y > pdi->dend)
1108 if (pdi->blank[dstPane] >= 0)
1109 currentPosDst.y -= pdi->dend - pdi->blank[dstPane] + 1;
1110 else if (pdi->blank[srcPane] >= 0)
1111 currentPosDst.y -= pdi->dend - pdi->blank[srcPane] + 1;
1113 // Group merge with previous (merge undo data to one action)
1114 if (!ListCopy(srcPane, dstPane, -1, bGroupWithPrevious, false))
1115 break; // sync failure
1116 if (!bGroupWithPrevious)
1117 bGroupWithPrevious = true;
1120 if (pdi->op == OP_DIFF)
1121 ++unresolvedConflictCount;
1124 ForEachView(dstPane, [currentPosDst](auto& pView) {
1125 pView->SetCursorPos(currentPosDst);
1126 pView->SetNewSelection(currentPosDst, currentPosDst, false);
1127 pView->SetNewAnchor(currentPosDst);
1130 suppressRescan.Clear(); // done suppress Rescan
1132 UpdateHeaderPath(dstPane);
1134 if (autoMergedCount > 0)
1135 m_bAutoMerged = true;
1137 // move to first conflict
1138 const int nDiff = m_diffList.FirstSignificant3wayDiff(THREEWAYDIFFTYPE_CONFLICT);
1140 pViewDst->SelectDiff(nDiff, true, false);
1143 strutils::format_string2(
1144 _("The number of automatically merged changes: %1\nThe number of unresolved conflicts: %2"),
1145 strutils::format(_T("%d"), autoMergedCount),
1146 strutils::format(_T("%d"), unresolvedConflictCount)),
1147 MB_ICONINFORMATION);
1151 * @brief Sanity check difference.
1153 * Checks that lines in difference are inside difference in both files.
1154 * If file is edited, lines added or removed diff lines get out of sync and
1155 * merging fails miserably.
1157 * @param [in] dr Difference to check.
1158 * @return true if difference lines match, false otherwise.
1160 bool CMergeDoc::SanityCheckDiff(DIFFRANGE dr) const
1162 const int cd_dbegin = dr.dbegin;
1163 const int cd_dend = dr.dend;
1165 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1167 // Must ensure line number is in range before getting line flags
1168 if (cd_dend >= m_ptBuf[nBuffer]->GetLineCount())
1171 // Optimization - check last line first so we don't need to
1172 // check whole diff for obvious cases
1173 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1174 if (!(dwFlags & LF_WINMERGE_FLAGS))
1178 for (int line = cd_dbegin; line < cd_dend; line++)
1180 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
1182 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(cd_dend);
1183 if (!(dwFlags & LF_WINMERGE_FLAGS))
1191 * @brief Copy selected (=current) difference from from side to side.
1192 * @param [in] srcPane Source side from which diff is copied
1193 * @param [in] dstPane Destination side
1194 * @param [in] nDiff Diff to copy, if -1 function determines it.
1195 * @param [in] bGroupWithPrevious Adds diff to same undo group with
1196 * @return true if ok, false if sync failure & need to abort copy
1197 * previous action (allows one undo for copy all)
1199 bool CMergeDoc::ListCopy(int srcPane, int dstPane, int nDiff /* = -1*/,
1200 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1202 int nGroup = GetActiveMergeView()->m_nThisGroup;
1203 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1204 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1205 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1207 // suppress Rescan during this method
1208 // (Not only do we not want to rescan a lot of times, but
1209 // it will wreck the line status array to rescan as we merge)
1210 RescanSuppress suppressRescan(*this);
1212 // If diff-number not given, determine it from active view
1215 nDiff = GetCurrentDiff();
1217 // No current diff, but maybe cursor is in diff?
1218 if (nDiff == -1 && (pViewSrc->IsCursorInDiff() ||
1219 pViewDst->IsCursorInDiff()))
1221 // Find out diff under cursor
1222 CPoint ptCursor = GetActiveMergeView()->GetCursorPos();
1223 nDiff = m_diffList.LineToDiff(ptCursor.y);
1230 VERIFY(m_diffList.GetDiff(nDiff, cd));
1231 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1232 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1233 bool bSrcWasMod = sbuf.IsModified();
1234 const int cd_dbegin = cd.dbegin;
1235 const int cd_dend = cd.dend;
1236 const int cd_blank = cd.blank[srcPane];
1237 bool bInSync = SanityCheckDiff(cd);
1241 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1242 return false; // abort copying
1245 // If we remove whole diff from current view, we must fix cursor
1246 // position first. Normally we would move to end of previous line,
1247 // but we want to move to begin of that line for usability.
1250 CPoint currentPos = pViewDst->GetCursorPos();
1252 if (currentPos.y > cd_dend)
1254 if (cd.blank[dstPane] >= 0)
1255 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1256 else if (cd.blank[srcPane] >= 0)
1257 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1259 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1262 // if the current diff contains missing lines, remove them from both sides
1263 int limit = cd_dend;
1265 // curView is the view which is changed, so the opposite of the source view
1266 dbuf.BeginUndoGroup(bGroupWithPrevious);
1269 // text was missing, so delete rest of lines on both sides
1270 // delete only on destination side since rescan will clear the other side
1271 if (cd_dend + 1 < dbuf.GetLineCount())
1273 dbuf.DeleteText(pSource, cd_blank, 0, cd_dend+1, 0, CE_ACTION_MERGE);
1277 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1278 ASSERT(cd_blank > 0);
1279 dbuf.DeleteText(pSource, cd_blank-1, dbuf.GetLineLength(cd_blank-1), cd_dend, dbuf.GetLineLength(cd_dend), CE_ACTION_MERGE);
1283 dbuf.FlushUndoGroup(pSource);
1284 dbuf.BeginUndoGroup(true);
1288 // copy the selected text over
1289 if (cd_dbegin <= limit)
1291 // text exists on left side, so just replace
1292 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1293 dbuf.FlushUndoGroup(pSource);
1294 dbuf.BeginUndoGroup(true);
1296 dbuf.FlushUndoGroup(pSource);
1301 // reset the mod status of the source view because we do make some
1302 // changes, but none that concern the source text
1303 sbuf.SetModified(bSrcWasMod);
1306 suppressRescan.Clear(); // done suppress Rescan
1311 bool CMergeDoc::PartialListCopy(int srcPane, int dstPane, int nDiff, int firstLine, int lastLine /*= -1*/,
1312 bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1314 int nGroup = GetActiveMergeView()->m_nThisGroup;
1315 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1316 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1317 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1319 // suppress Rescan during this method
1320 // (Not only do we not want to rescan a lot of times, but
1321 // it will wreck the line status array to rescan as we merge)
1322 RescanSuppress suppressRescan(*this);
1325 VERIFY(m_diffList.GetDiff(nDiff, cd));
1326 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1327 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1328 bool bSrcWasMod = sbuf.IsModified();
1329 const int cd_dbegin = (firstLine > cd.dbegin) ? firstLine : cd.dbegin;
1330 const int cd_dend = cd.dend;
1331 const int cd_blank = cd.blank[srcPane];
1332 bool bInSync = SanityCheckDiff(cd);
1336 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1337 return false; // abort copying
1340 // If we remove whole diff from current view, we must fix cursor
1341 // position first. Normally we would move to end of previous line,
1342 // but we want to move to begin of that line for usability.
1345 CPoint currentPos = pViewDst->GetCursorPos();
1347 if (currentPos.y > cd_dend)
1349 if (cd.blank[dstPane] >= 0)
1350 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1351 else if (cd.blank[srcPane] >= 0)
1352 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1354 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1357 // if the current diff contains missing lines, remove them from both sides
1358 int limit = ((lastLine < 0) || (lastLine > cd_dend)) ? cd_dend : lastLine;
1360 // curView is the view which is changed, so the opposite of the source view
1361 dbuf.BeginUndoGroup(bGroupWithPrevious);
1362 if ((cd_blank >= 0) && (cd_dbegin >= cd_blank))
1364 // text was missing, so delete rest of lines on both sides
1365 // delete only on destination side since rescan will clear the other side
1366 if (limit+1 < dbuf.GetLineCount())
1368 dbuf.DeleteText(pSource, cd_dbegin, 0, limit+1, 0, CE_ACTION_MERGE);
1372 // To removing EOL chars of last line, deletes from the end of the line (cd_blank - 1).
1373 ASSERT(cd_dbegin > 0);
1374 dbuf.DeleteText(pSource, cd_dbegin-1, dbuf.GetLineLength(cd_dbegin-1), limit, dbuf.GetLineLength(limit), CE_ACTION_MERGE);
1377 limit = cd_dbegin-1;
1378 dbuf.FlushUndoGroup(pSource);
1379 dbuf.BeginUndoGroup(true);
1382 // copy the selected text over
1383 if (cd_dbegin <= limit)
1385 // text exists on left side, so just replace
1386 dbuf.ReplaceFullLines(dbuf, sbuf, pSource, cd_dbegin, limit, CE_ACTION_MERGE);
1387 dbuf.FlushUndoGroup(pSource);
1388 dbuf.BeginUndoGroup(true);
1390 dbuf.FlushUndoGroup(pSource);
1395 // reset the mod status of the source view because we do make some
1396 // changes, but none that concern the source text
1397 sbuf.SetModified(bSrcWasMod);
1399 suppressRescan.Clear(); // done suppress Rescan
1404 bool CMergeDoc::WordListCopy(int srcPane, int dstPane, int nDiff, int firstWordDiff, int lastWordDiff,
1405 const std::vector<int> *pWordDiffIndice, bool bGroupWithPrevious /*= false*/, bool bUpdateView /*= true*/)
1407 int nGroup = GetActiveMergeView()->m_nThisGroup;
1408 CMergeEditView *pViewSrc = m_pView[nGroup][srcPane];
1409 CMergeEditView *pViewDst = m_pView[nGroup][dstPane];
1410 CCrystalTextView *pSource = bUpdateView ? pViewDst : nullptr;
1412 // suppress Rescan during this method
1413 // (Not only do we not want to rescan a lot of times, but
1414 // it will wreck the line status array to rescan as we merge)
1415 RescanSuppress suppressRescan(*this);
1418 VERIFY(m_diffList.GetDiff(nDiff, cd));
1419 CDiffTextBuffer& sbuf = *m_ptBuf[srcPane];
1420 CDiffTextBuffer& dbuf = *m_ptBuf[dstPane];
1421 bool bSrcWasMod = sbuf.IsModified();
1422 const int cd_dbegin = cd.dbegin;
1423 const int cd_dend = cd.dend;
1424 const int cd_blank = cd.blank[srcPane];
1425 bool bInSync = SanityCheckDiff(cd);
1429 LangMessageBox(IDS_VIEWS_OUTOFSYNC, MB_ICONSTOP);
1430 return false; // abort copying
1433 std::vector<WordDiff> worddiffs = GetWordDiffArrayInDiffBlock(nDiff);
1435 if (worddiffs.empty())
1438 if (cd.end[srcPane] < cd.begin[srcPane])
1439 return ListCopy(srcPane, dstPane, nDiff, bGroupWithPrevious, bUpdateView);
1441 if (firstWordDiff == -1)
1443 if (lastWordDiff == -1)
1444 lastWordDiff = static_cast<int>(worddiffs.size() - 1);
1446 // If we remove whole diff from current view, we must fix cursor
1447 // position first. Normally we would move to end of previous line,
1448 // but we want to move to begin of that line for usability.
1451 CPoint currentPos = pViewDst->GetCursorPos();
1453 if (currentPos.y > cd_dend)
1455 if (cd.blank[dstPane] >= 0)
1456 currentPos.y -= cd_dend - cd.blank[dstPane] + 1;
1457 else if (cd.blank[srcPane] >= 0)
1458 currentPos.y -= cd_dend - cd.blank[srcPane] + 1;
1460 ForEachView(dstPane, [currentPos](auto& pView) { pView->SetCursorPos(currentPos); });
1463 // if the current diff contains missing lines, remove them from both sides
1464 int limit = cd_dend;
1466 // curView is the view which is changed, so the opposite of the source view
1467 dbuf.BeginUndoGroup(bGroupWithPrevious);
1469 CString srcText, dstText;
1470 CPoint ptDstStart, ptDstEnd;
1471 CPoint ptSrcStart, ptSrcEnd;
1473 ptDstStart.x = worddiffs[firstWordDiff].begin[dstPane];
1474 ptDstStart.y = worddiffs[firstWordDiff].beginline[dstPane];
1475 ptDstEnd.x = worddiffs[lastWordDiff].end[dstPane];
1476 ptDstEnd.y = worddiffs[lastWordDiff].endline[dstPane];
1477 ptSrcStart.x = worddiffs[firstWordDiff].begin[srcPane];
1478 ptSrcStart.y = worddiffs[firstWordDiff].beginline[srcPane];
1479 ptSrcEnd.x = worddiffs[lastWordDiff].end[srcPane];
1480 ptSrcEnd.y = worddiffs[lastWordDiff].endline[srcPane];
1482 std::vector<int> nDstOffsets(ptDstEnd.y - ptDstStart.y + 2);
1483 std::vector<int> nSrcOffsets(ptSrcEnd.y - ptSrcStart.y + 2);
1485 dbuf.GetTextWithoutEmptys(ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, dstText);
1486 sbuf.GetTextWithoutEmptys(ptSrcStart.y, ptSrcStart.x, ptSrcEnd.y, ptSrcEnd.x, srcText);
1489 for (int nLine = ptDstStart.y; nLine <= ptDstEnd.y; nLine++)
1490 nDstOffsets[nLine-ptDstStart.y+1] = nDstOffsets[nLine-ptDstStart.y] + dbuf.GetFullLineLength(nLine);
1492 for (int nLine = ptSrcStart.y; nLine <= ptSrcEnd.y; nLine++)
1493 nSrcOffsets[nLine-ptSrcStart.y+1] = nSrcOffsets[nLine-ptSrcStart.y] + sbuf.GetFullLineLength(nLine);
1495 for (int i = lastWordDiff; i != firstWordDiff-1; --i)
1497 if (pWordDiffIndice && std::find(pWordDiffIndice->begin(), pWordDiffIndice->end(), i) == pWordDiffIndice->end())
1499 int srcBegin = nSrcOffsets[worddiffs[i].beginline[srcPane] - ptSrcStart.y] + worddiffs[i].begin[srcPane];
1500 int srcEnd = nSrcOffsets[worddiffs[i].endline[srcPane] - ptSrcStart.y] + worddiffs[i].end[srcPane];
1501 int dstBegin = nDstOffsets[worddiffs[i].beginline[dstPane] - ptDstStart.y] + worddiffs[i].begin[dstPane];
1502 int dstEnd = nDstOffsets[worddiffs[i].endline[dstPane] - ptDstStart.y] + worddiffs[i].end[dstPane];
1503 dstText = dstText.Mid(0, dstBegin - ptDstStart.x)
1504 + srcText.Mid(srcBegin - ptSrcStart.x, srcEnd - srcBegin)
1505 + dstText.Mid(dstEnd - ptDstStart.x);
1508 dbuf.DeleteText(pSource, ptDstStart.y, ptDstStart.x, ptDstEnd.y, ptDstEnd.x, CE_ACTION_MERGE);
1511 dbuf.InsertText(pSource, ptDstStart.y, ptDstStart.x, dstText, dstText.GetLength(), endl, endc, CE_ACTION_MERGE);
1513 dbuf.FlushUndoGroup(pSource);
1515 // reset the mod status of the source view because we do make some
1516 // changes, but none that concern the source text
1517 sbuf.SetModified(bSrcWasMod);
1519 suppressRescan.Clear(); // done suppress Rescan
1526 * @brief Save file with new filename.
1528 * This function is called by CMergeDoc::DoSave() or CMergeDoc::DoSAveAs()
1529 * to save file with new filename. CMergeDoc::DoSave() calls if saving with
1530 * normal filename fails, to let user choose another filename/location.
1531 * Also, if file is unnamed file (e.g. scratchpad) then it must be saved
1532 * using this function.
1533 * @param [in, out] strPath
1534 * - [in] : Initial path shown to user
1535 * - [out] : Path to new filename if saving succeeds
1536 * @param [in, out] nSaveResult
1537 * - [in] : Statuscode telling why we ended up here. Maybe the result of
1539 * - [out] : Statuscode of this saving try
1540 * @param [in, out] sError Error string from lower level saving code
1541 * @param [in] nBuffer Buffer we are saving
1542 * @return false as long as the user is not satisfied. Calling function
1543 * should not continue until true is returned.
1544 * @sa CMergeDoc::DoSave()
1545 * @sa CMergeDoc::DoSaveAs()
1546 * @sa CMergeDoc::CDiffTextBuffer::SaveToFile()
1548 bool CMergeDoc::TrySaveAs(String &strPath, int &nSaveResult, String & sError,
1549 int nBuffer, PackingInfo& infoTempUnpacker)
1553 String strSavePath; // New path for next saving try
1556 int answer = IDOK; // Set default we use for scratchpads
1558 // We shouldn't get here if saving is succeed before
1559 ASSERT(nSaveResult != SAVE_DONE);
1561 // Select message based on reason function called
1562 if (nSaveResult == SAVE_PACK_FAILED)
1564 str = CMergeApp::GetPackingErrorMessage(nBuffer, m_nBuffers, strPath, infoTempUnpacker);
1565 // replace the unpacker with a "do nothing" unpacker
1566 infoTempUnpacker.Initialize(false);
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_infoUnpacker
1663 // if an error arises during packing, change and take a "do nothing" packer
1664 PackingInfo infoTempUnpacker = m_infoUnpacker;
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 = CMergeApp::HandleReadonlySave(strSavePath, false, bApplyToAll);
1684 if (nRetVal == IDCANCEL)
1687 if (!CMergeApp::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_infoUnpacker
1777 // if an error arises during packing, change and take a "do nothing" packer
1778 PackingInfo infoTempUnpacker = m_infoUnpacker;
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 Change number of diff context lines
2126 void CMergeDoc::OnDiffContext(UINT nID)
2130 case ID_VIEW_DIFFCONTEXT_0:
2131 m_nDiffContext = 0; break;
2132 case ID_VIEW_DIFFCONTEXT_1:
2133 m_nDiffContext = 1; break;
2134 case ID_VIEW_DIFFCONTEXT_3:
2135 m_nDiffContext = 3; break;
2136 case ID_VIEW_DIFFCONTEXT_5:
2137 m_nDiffContext = 5; break;
2138 case ID_VIEW_DIFFCONTEXT_7:
2139 m_nDiffContext = 7; break;
2140 case ID_VIEW_DIFFCONTEXT_9:
2141 m_nDiffContext = 9; break;
2142 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2143 m_nDiffContext = -m_nDiffContext - 1; break;
2144 case ID_VIEW_DIFFCONTEXT_ALL:
2145 if (m_nDiffContext >= 0)
2146 m_nDiffContext = -m_nDiffContext - 1;
2148 case ID_VIEW_DIFFCONTEXT_INVERT:
2149 m_bInvertDiffContext = !m_bInvertDiffContext;
2152 GetOptionsMgr()->SaveOption(OPT_DIFF_CONTEXT, m_nDiffContext);
2153 GetOptionsMgr()->SaveOption(OPT_INVERT_DIFF_CONTEXT, m_bInvertDiffContext);
2154 FlushAndRescan(true);
2158 * @brief Swap the positions of the two panes
2160 template<int srcPane, int dstPane>
2161 void CMergeDoc::OnViewSwapPanes()
2163 SwapFiles(srcPane, dstPane);
2167 * @brief Swap context enable for 3 file compares
2169 void CMergeDoc::OnUpdateSwapContext(CCmdUI* pCmdUI)
2171 pCmdUI->Enable(m_nBuffers > 2);
2175 * @brief Update number of diff context lines
2177 void CMergeDoc::OnUpdateDiffContext(CCmdUI* pCmdUI)
2180 switch (pCmdUI->m_nID)
2182 case ID_VIEW_DIFFCONTEXT_0:
2183 bCheck = (m_nDiffContext == 0); break;
2184 case ID_VIEW_DIFFCONTEXT_1:
2185 bCheck = (m_nDiffContext == 1); break;
2186 case ID_VIEW_DIFFCONTEXT_3:
2187 bCheck = (m_nDiffContext == 3); break;
2188 case ID_VIEW_DIFFCONTEXT_5:
2189 bCheck = (m_nDiffContext == 5); break;
2190 case ID_VIEW_DIFFCONTEXT_7:
2191 bCheck = (m_nDiffContext == 7); break;
2192 case ID_VIEW_DIFFCONTEXT_9:
2193 bCheck = (m_nDiffContext == 9); break;
2194 case ID_VIEW_DIFFCONTEXT_TOGGLE:
2195 bCheck = false; break;
2196 case ID_VIEW_DIFFCONTEXT_INVERT:
2197 bCheck = m_bInvertDiffContext; break;
2199 bCheck = (m_nDiffContext < 0); break;
2201 pCmdUI->SetCheck(bCheck);
2202 pCmdUI->Enable(!(pCmdUI->m_nID == ID_VIEW_DIFFCONTEXT_INVERT && (m_nDiffContext < 0)));
2206 * @brief Build the diff array and prepare buffers accordingly (insert ghost lines, set WinMerge flags)
2208 * @note After PrimeTextBuffers(), all buffers should have the same length.
2210 void CMergeDoc::PrimeTextBuffers()
2213 m_nTrivialDiffs = 0;
2215 int nDiffCount = m_diffList.GetSize();
2218 // walk the diff list and calculate numbers of extra lines to add
2219 int extras[3] = {0, 0, 0}; // extra lines added to each view
2220 m_diffList.GetExtraLinesCounts(m_nBuffers, extras);
2222 // resize m_aLines once for each view
2223 UINT lcount[3] = {0, 0, 0};
2224 UINT lcountnew[3] = {0, 0, 0};
2227 for (file = 0; file < m_nBuffers; file++)
2229 lcount[file] = m_ptBuf[file]->GetLineCount();
2230 lcountnew[file] = lcount[file] + extras[file];
2231 lcountmax = max(lcountmax, lcountnew[file]);
2233 for (file = 0; file < m_nBuffers; file++)
2235 m_ptBuf[file]->m_aLines.resize(lcountmax);
2238 // walk the diff list backward, move existing lines to proper place,
2239 // add ghost lines, and set flags
2240 for (nDiff = nDiffCount - 1; nDiff >= 0; nDiff --)
2243 VERIFY(m_diffList.GetDiff(nDiff, curDiff));
2245 // move matched lines after curDiff
2246 int nline[3] = { 0, 0, 0 };
2247 for (file = 0; file < m_nBuffers; file++)
2248 nline[file] = lcount[file] - curDiff.end[file] - 1; // #lines on left/middle/right after current diff
2249 // Matched lines should really match...
2250 // But matched lines after last diff may differ because of empty last line (see function's note)
2251 if (nDiff < nDiffCount - 1)
2252 ASSERT(nline[0] == nline[1]);
2255 for (file = 0; file < m_nBuffers; file++)
2257 // Move all lines after current diff down as far as needed
2258 // for any ghost lines we're about to insert
2259 m_ptBuf[file]->MoveLine(curDiff.end[file]+1, lcount[file]-1, lcountnew[file]-nline[file]);
2260 lcountnew[file] -= nline[file];
2261 lcount[file] -= nline[file];
2262 // move unmatched lines and add ghost lines
2263 nline[file] = curDiff.end[file] - curDiff.begin[file] + 1; // #lines in diff on left/middle/right
2264 nmaxline = max(nmaxline, nline[file]);
2267 for (file = 0; file < m_nBuffers; file++)
2269 DWORD dflag = LF_GHOST;
2270 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2272 m_ptBuf[file]->MoveLine(curDiff.begin[file], curDiff.end[file], lcountnew[file]-nmaxline);
2273 int nextra = nmaxline - nline[file];
2276 m_ptBuf[file]->SetEmptyLine(lcountnew[file] - nextra, nextra);
2277 for (int i = 1; i <= nextra; i++)
2278 m_ptBuf[file]->SetLineFlag(lcountnew[file]-i, dflag, true, false, false);
2280 lcountnew[file] -= nmaxline;
2282 lcount[file] -= nline[file];
2285 // set dbegin, dend, blank, and line flags
2286 curDiff.dbegin = lcountnew[0];
2299 curDiff.dend = lcountnew[0]+nmaxline-1;
2300 for (file = 0; file < m_nBuffers; file++)
2302 curDiff.blank[file] = -1;
2303 int nextra = nmaxline - nline[file];
2304 if (nmaxline > nline[file])
2306 // more lines on left, ghost lines on right side
2307 curDiff.blank[file] = curDiff.dend + 1 - nextra;
2313 for (file = 0; file < m_nBuffers; file++)
2317 for (i = curDiff.dbegin; i <= curDiff.dend; i++)
2319 if (curDiff.blank[file] == -1 || (int)i < curDiff.blank[file])
2321 // set diff or trivial flag
2322 DWORD dflag = (curDiff.op == OP_TRIVIAL) ? LF_TRIVIAL : LF_DIFF;
2323 if ((file == 0 && curDiff.op == OP_3RDONLY) || (file == 2 && curDiff.op == OP_1STONLY))
2325 m_ptBuf[file]->SetLineFlag(i, dflag, true, false, false);
2326 m_ptBuf[file]->SetLineFlag(i, LF_INVISIBLE, false, false, false);
2330 // ghost lines are already inserted (and flagged)
2331 // ghost lines opposite to trivial lines are ghost and trivial
2332 if (curDiff.op == OP_TRIVIAL)
2333 m_ptBuf[file]->SetLineFlag(i, LF_TRIVIAL, true, false, false);
2339 } // switch (curDiff.op)
2340 VERIFY(m_diffList.SetDiff(nDiff, curDiff));
2341 } // for (nDiff = nDiffCount; nDiff-- > 0; )
2343 m_diffList.ConstructSignificantChain();
2346 // Note: By this point all `m_ptBuf[]` buffers must have the same
2347 // number of line entries; eventual buffer processing typically
2348 // uses the line count from `m_ptBuf[0]` for all buffer processing.
2350 for (file = 0; file < m_nBuffers; file++)
2352 ASSERT(m_ptBuf[0]->GetLineCount() == m_ptBuf[file]->GetLineCount());
2356 for (file = 0; file < m_nBuffers; file++)
2357 m_ptBuf[file]->FinishLoading();
2361 * @brief Checks if file has changed since last update (save or rescan).
2362 * @param [in] szPath File to check
2363 * @param [in] dfi Previous fileinfo of file
2364 * @param [in] bSave If true Compare to last save-info, else to rescan-info
2365 * @param [in] nBuffer Index (0-based) of buffer
2366 * @return true if file is changed.
2368 CMergeDoc::FileChange CMergeDoc::IsFileChangedOnDisk(LPCTSTR szPath, DiffFileInfo &dfi,
2369 bool bSave, int nBuffer)
2371 DiffFileInfo *fileInfo = nullptr;
2372 bool bFileChanged = false;
2373 bool bIgnoreSmallDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
2375 if (bIgnoreSmallDiff)
2376 tolerance = SmallTimeDiff; // From MainFrm.h
2379 fileInfo = m_pSaveFileInfo[nBuffer].get();
2381 fileInfo = m_pRescanFileInfo[nBuffer].get();
2383 // We assume file existed, so disappearing means removal
2384 if (!dfi.Update(szPath))
2385 return FileChange::Removed;
2387 int64_t timeDiff = dfi.mtime - fileInfo->mtime;
2388 if (timeDiff < 0) timeDiff = -timeDiff;
2389 if ((timeDiff > tolerance * Poco::Timestamp::resolution()) || (dfi.size != fileInfo->size))
2391 bFileChanged = true;
2395 return FileChange::Changed;
2397 return FileChange::NoChange;
2400 void CMergeDoc::HideLines()
2405 if (m_nDiffContext < 0)
2407 ForEachView([](auto& pView) { pView->SetEnableHideLines(false); });
2411 int nLineCount = 0x7fffffff;
2412 for (file = 0; file < m_nBuffers; file++)
2414 if (nLineCount > m_ptBuf[file]->GetLineCount())
2415 nLineCount = m_ptBuf[file]->GetLineCount();
2418 for (nLine = 0; nLine < nLineCount;)
2420 bool diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2421 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2423 for (file = 0; file < m_nBuffers; file++)
2424 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, true, false, false);
2429 int nLine2 = (nLine - m_nDiffContext < 0) ? 0 : (nLine - m_nDiffContext);
2430 for (; nLine2 < nLine; nLine2++)
2432 for (file = 0; file < m_nBuffers; file++)
2433 m_ptBuf[file]->SetLineFlag(nLine2, LF_INVISIBLE, false, false, false);
2436 for (; nLine < nLineCount; nLine++)
2438 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2439 if ((!m_bInvertDiffContext && !diff) || (m_bInvertDiffContext && diff))
2441 for (file = 0; file < m_nBuffers; file++)
2442 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2445 int nLineEnd2 = (nLine + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + m_nDiffContext);
2446 for (; nLine < nLineEnd2; nLine++)
2448 for (file = 0; file < m_nBuffers; file++)
2449 m_ptBuf[file]->SetLineFlag(nLine, LF_INVISIBLE, false, false, false);
2450 diff = !!(m_ptBuf[0]->GetLineFlags(nLine) & (LF_DIFF | LF_GHOST));
2451 if ((!m_bInvertDiffContext && diff) || (m_bInvertDiffContext && !diff))
2452 nLineEnd2 = (nLine + 1 + m_nDiffContext >= nLineCount) ? nLineCount-1 : (nLine + 1 + m_nDiffContext);
2457 ForEachView([](auto& pView) { pView->SetEnableHideLines(true); });
2461 * @brief Asks and then saves modified files.
2463 * This function saves modified files. Dialog is shown for user to select
2464 * modified file(s) one wants to save or discard changed. Cancelling of
2465 * save operation is allowed unless denied by parameter. After successfully
2466 * save operation file statuses are updated to directory compare.
2467 * @param [in] bAllowCancel If false "Cancel" button is disabled.
2468 * @return true if user selected "OK" so next operation can be
2469 * executed. If false user choosed "Cancel".
2470 * @note If filename is empty, we assume scratchpads are saved,
2471 * so instead of filename, description is shown.
2472 * @todo If we have filename and description for file, what should
2473 * we do after saving to different filename? Empty description?
2474 * @todo Parameter @p bAllowCancel is always true in callers - can be removed.
2476 bool CMergeDoc::PromptAndSaveIfNeeded(bool bAllowCancel)
2478 bool bLModified = false, bMModified = false, bRModified = false;
2480 bool bLSaveSuccess = false, bMSaveSuccess = false, bRSaveSuccess = false;
2482 if (m_nBuffers == 3)
2484 bLModified = m_ptBuf[0]->IsModified();
2485 bMModified = m_ptBuf[1]->IsModified();
2486 bRModified = m_ptBuf[2]->IsModified();
2490 bLModified = m_ptBuf[0]->IsModified();
2491 bRModified = m_ptBuf[1]->IsModified();
2493 if (!bLModified && !bMModified && !bRModified)
2497 dlg.DoAskFor(bLModified, bMModified, bRModified);
2499 dlg.m_bDisableCancel = true;
2500 if (!m_filePaths.GetLeft().empty())
2502 if (theApp.m_strSaveAsPath.empty())
2503 dlg.m_sLeftFile = m_filePaths.GetLeft();
2505 dlg.m_sLeftFile = theApp.m_strSaveAsPath;
2508 dlg.m_sLeftFile = m_strDesc[0];
2509 if (m_nBuffers == 3)
2511 if (!m_filePaths.GetMiddle().empty())
2513 if (theApp.m_strSaveAsPath.empty())
2514 dlg.m_sMiddleFile = m_filePaths.GetMiddle();
2516 dlg.m_sMiddleFile = theApp.m_strSaveAsPath;
2519 dlg.m_sMiddleFile = m_strDesc[1];
2521 if (!m_filePaths.GetRight().empty())
2523 if (theApp.m_strSaveAsPath.empty())
2524 dlg.m_sRightFile = m_filePaths.GetRight();
2526 dlg.m_sRightFile = theApp.m_strSaveAsPath;
2529 dlg.m_sRightFile = m_strDesc[m_nBuffers - 1];
2531 if (dlg.DoModal() == IDOK)
2533 if (bLModified && dlg.m_leftSave == SaveClosingDlg::SAVECLOSING_SAVE)
2535 if (!DoSave(m_filePaths.GetLeft().c_str(), bLSaveSuccess, 0))
2539 if (bMModified && dlg.m_middleSave == SaveClosingDlg::SAVECLOSING_SAVE)
2541 if (!DoSave(m_filePaths.GetMiddle().c_str(), bMSaveSuccess, 1))
2545 if (bRModified && dlg.m_rightSave == SaveClosingDlg::SAVECLOSING_SAVE)
2547 if (!DoSave(m_filePaths.GetRight().c_str(), bRSaveSuccess, m_nBuffers - 1))
2556 // If file were modified and saving was successfull,
2557 // update status on dir view
2558 if ((bLModified && bLSaveSuccess) ||
2559 (bMModified && bMSaveSuccess) ||
2560 (bRModified && bRSaveSuccess))
2562 // If directory compare has results
2563 if (m_pDirDoc != nullptr && m_pDirDoc->HasDiffs())
2565 if (m_bEditAfterRescan[0] || m_bEditAfterRescan[1] || (m_nBuffers == 3 && m_bEditAfterRescan[2]))
2566 FlushAndRescan(false);
2568 bool bIdentical = !m_diffList.HasSignificantDiffs(); // True if status should be set to identical
2569 m_pDirDoc->UpdateChangedItem(m_filePaths, m_diffList.GetSignificantDiffs(),
2570 m_nTrivialDiffs, bIdentical);
2577 /** Rescan only if we did not Rescan during the last timeOutInSecond seconds*/
2578 void CMergeDoc::RescanIfNeeded(float timeOutInSecond)
2580 // if we did not rescan during the request timeOut, Rescan
2581 // else we did Rescan after the request, so do nothing
2582 COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - m_LastRescan;
2583 if (elapsed.GetTotalSeconds() >= timeOutInSecond)
2584 // (laoran 08-01-2003) maybe should be FlushAndRescan(true) ??
2589 * @brief We have two child views (left & right), so we keep pointers directly
2590 * at them (the MFC view list doesn't have them both)
2592 void CMergeDoc::AddMergeViews(CMergeEditView *pView[3])
2595 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2596 m_pView[m_nGroups][nBuffer] = pView[nBuffer];
2600 void CMergeDoc::RemoveMergeViews(int nGroup)
2603 for (; nGroup < m_nGroups - 1; nGroup++)
2605 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2607 m_pView[nGroup][nBuffer] = m_pView[nGroup + 1][nBuffer];
2608 m_pView[nGroup][nBuffer]->m_nThisGroup = nGroup;
2615 * @brief DirDoc gives us its identity just after it creates us
2617 void CMergeDoc::SetDirDoc(CDirDoc * pDirDoc)
2619 ASSERT(pDirDoc != nullptr && m_pDirDoc == nullptr);
2620 m_pDirDoc = pDirDoc;
2624 * @brief Return pointer to parent frame
2626 CMergeEditFrame * CMergeDoc::GetParentFrame()
2628 return dynamic_cast<CMergeEditFrame *>(m_pView[0][0]->GetParentFrame());
2632 * @brief DirDoc is closing
2634 void CMergeDoc::DirDocClosing(CDirDoc * pDirDoc)
2636 ASSERT(m_pDirDoc == pDirDoc);
2637 m_pDirDoc = nullptr;
2638 // TODO (Perry 2003-03-30): perhaps merge doc should close now ?
2642 * @brief DirDoc commanding us to close
2644 bool CMergeDoc::CloseNow()
2646 // Allow user to cancel closing
2647 if (!PromptAndSaveIfNeeded(true))
2650 GetParentFrame()->DestroyWindow();
2655 * @brief Loads file to buffer and shows load-errors
2656 * @param [in] sFileName File to open
2657 * @param [in] nBuffer Index (0-based) of buffer to load
2658 * @param [out] readOnly whether file is read-only
2659 * @param [in] encoding encoding used
2660 * @return Tells if files were loaded successfully
2661 * @sa CMergeDoc::OpenDocs()
2663 int CMergeDoc::LoadFile(CString sFileName, int nBuffer, bool & readOnly, const FileTextEncoding & encoding)
2666 DWORD retVal = FileLoadResult::FRESULT_ERROR;
2668 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
2669 m_filePaths[nBuffer] = sFileName;
2671 CRLFSTYLE nCrlfStyle = CRLFSTYLE::AUTOMATIC;
2673 retVal = pBuf->LoadFromFile(sFileName, m_infoUnpacker,
2674 m_strBothFilenames.c_str(), readOnly, nCrlfStyle, encoding, sOpenError);
2676 // if CMergeDoc::CDiffTextBuffer::LoadFromFile failed,
2677 // it left the pBuf in a valid (but empty) state via a call to InitNew
2679 if (FileLoadResult::IsOkImpure(retVal))
2681 // File loaded, and multiple EOL types in this file
2682 FileLoadResult::SetMainOk(retVal);
2684 // If mixed EOLs are not enabled, enable them for this doc.
2685 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
2687 pBuf->SetMixedEOL(true);
2691 if (FileLoadResult::IsError(retVal))
2693 // Error from Unifile/system
2694 if (!sOpenError.IsEmpty())
2695 sError = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), (LPCTSTR)sFileName, (LPCTSTR)sOpenError);
2697 sError = strutils::format_string1(_("File not found: %1"), (LPCTSTR)sFileName);
2698 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2700 else if (FileLoadResult::IsErrorUnpack(retVal))
2702 sError = strutils::format_string1(_("File not unpacked: %1"), (LPCTSTR)sFileName);
2703 ShowMessageBox(sError, MB_OK | MB_ICONSTOP | MB_MODELESS);
2709 * @brief Check if specified codepage number is valid for WinMerge Editor.
2710 * @param [in] cp Codepage number to check.
2711 * @return true if codepage is valid, false otherwise.
2713 bool CMergeDoc::IsValidCodepageForMergeEditor(unsigned cp) const
2715 if (cp == 0) // 0 is our signal value for invalid
2717 return GetEncodingNameFromCodePage(cp) != nullptr;
2721 * @brief Sanity check file's specified codepage.
2722 * This function checks if file's specified codepage is valid for WinMerge
2723 * editor and if not resets the codepage to default.
2724 * @param [in,out] fileinfo Class containing file's codepage.
2726 void CMergeDoc::SanityCheckCodepage(FileLocation & fileinfo)
2728 if (fileinfo.encoding.m_unicoding == ucr::NONE
2729 && !IsValidCodepageForMergeEditor(fileinfo.encoding.m_codepage))
2731 int cp = ucr::getDefaultCodepage();
2732 if (!IsValidCodepageForMergeEditor(cp))
2734 fileinfo.encoding.SetCodepage(cp);
2739 * @brief Loads one file from disk and updates file infos.
2740 * @param [in] index Index of file in internal buffers.
2741 * @param [in] filename File's name.
2742 * @param [in] readOnly Is file read-only?
2743 * @param [in] encoding File's encoding.
2744 * @return One of FileLoadResult values.
2746 DWORD CMergeDoc::LoadOneFile(int index, String filename, bool readOnly, const String& strDesc,
2747 const FileTextEncoding & encoding)
2749 DWORD loadSuccess = FileLoadResult::FRESULT_ERROR;;
2751 m_strDesc[index] = strDesc;
2752 if (!filename.empty())
2754 if (strDesc.empty())
2755 m_nBufferType[index] = BUFFERTYPE::NORMAL;
2757 m_nBufferType[index] = BUFFERTYPE::NORMAL_NAMED;
2758 m_pSaveFileInfo[index]->Update(filename);
2759 m_pRescanFileInfo[index]->Update(filename);
2761 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encoding);
2762 if (FileLoadResult::IsLossy(loadSuccess))
2764 // Determine the file encoding by looking at all the contents of the file, not just part of it
2765 FileTextEncoding encodingNew = codepage_detect::Guess(filename, GetOptionsMgr()->GetInt(OPT_CP_DETECT), -1);
2766 if (encoding != encodingNew)
2768 m_ptBuf[index]->FreeAll();
2769 loadSuccess = LoadFile(filename.c_str(), index, readOnly, encodingNew);
2775 m_nBufferType[index] = BUFFERTYPE::UNNAMED;
2776 m_ptBuf[index]->InitNew();
2777 m_ptBuf[index]->m_encoding = encoding;
2778 m_ptBuf[index]->FinishLoading(); // should clear GGhostTextBuffer::m_RealityBlock when reloading unnamed buffer
2779 loadSuccess = FileLoadResult::FRESULT_OK;
2784 void CMergeDoc::SetTableProperties()
2786 struct TableProps { bool istable; TCHAR delimiter; TCHAR quote; bool allowNewlinesInQuotes; };
2787 auto getTablePropsByFileName = [](const String& path, const std::optional<bool>& enableTableEditing)-> TableProps
2789 const TCHAR quote = GetOptionsMgr()->GetString(OPT_CMP_TBL_QUOTE_CHAR).c_str()[0];
2790 FileFilterHelper filterCSV, filterTSV, filterDSV;
2791 bool allowNewlineIQuotes = GetOptionsMgr()->GetBool(OPT_CMP_TBL_ALLOW_NEWLINES_IN_QUOTES);
2792 const String csvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_CSV_FILEPATTERNS);
2793 if (!csvFilePattern.empty())
2795 filterCSV.UseMask(true);
2796 filterCSV.SetMask(csvFilePattern);
2797 if (filterCSV.includeFile(path))
2798 return { true, ',', quote, allowNewlineIQuotes };
2800 const String tsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_TSV_FILEPATTERNS);
2801 if (!tsvFilePattern.empty())
2803 filterTSV.UseMask(true);
2804 filterTSV.SetMask(tsvFilePattern);
2805 if (filterTSV.includeFile(path))
2806 return { true, '\t', quote, allowNewlineIQuotes };
2808 const String dsvFilePattern = GetOptionsMgr()->GetString(OPT_CMP_DSV_FILEPATTERNS);
2809 if (!dsvFilePattern.empty())
2811 filterDSV.UseMask(true);
2812 filterDSV.SetMask(dsvFilePattern);
2813 if (filterDSV.includeFile(path))
2814 return { true, GetOptionsMgr()->GetString(OPT_CMP_DSV_DELIM_CHAR).c_str()[0], quote };
2816 if (enableTableEditing.value_or(false))
2819 if (dlg.DoModal() == IDOK)
2820 return { true, dlg.m_sDelimiterChar.c_str()[0], dlg.m_sQuoteChar.c_str()[0], dlg.m_bAllowNewlinesInQuotes };
2822 return { false, 0, 0, false };
2825 TableProps tableProps[3] = {};
2826 int nTableFileIndex = -1;
2827 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2830 paths::FindExtension(m_ptBuf[nBuffer - 1]->GetTempFileName()) != paths::FindExtension(m_ptBuf[nBuffer]->GetTempFileName()))
2831 tableProps[nBuffer] = getTablePropsByFileName(m_ptBuf[nBuffer]->GetTempFileName(), m_bEnableTableEditing);
2833 tableProps[nBuffer] = tableProps[nBuffer - 1];
2834 if (tableProps[nBuffer].istable)
2835 nTableFileIndex = nBuffer;
2837 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2839 if (m_bEnableTableEditing.value_or(true) && nTableFileIndex >= 0)
2841 int i = tableProps[nBuffer].istable ? nBuffer : nTableFileIndex;
2842 m_ptBuf[nBuffer]->SetTableEditing(true);
2843 m_ptBuf[nBuffer]->ShareColumnWidths(*m_ptBuf[0]);
2844 m_ptBuf[nBuffer]->SetAllowNewlinesInQuotes(tableProps[i].allowNewlinesInQuotes);
2845 m_ptBuf[nBuffer]->SetFieldDelimiter(tableProps[i].delimiter);
2846 m_ptBuf[nBuffer]->SetFieldEnclosure(tableProps[i].quote);
2847 m_ptBuf[nBuffer]->JoinLinesForTableEditingMode();
2851 m_ptBuf[nBuffer]->SetTableEditing(false);
2857 * @brief Loads files and does initial rescan.
2858 * @param fileloc [in] File to open to left/middle/right side (path & encoding info)
2859 * @param bRO [in] Is left/middle/right file read-only
2860 * @return Success/Failure/Binary (failure) per typedef enum OpenDocsResult_TYPE
2861 * @todo Options are still read from CMainFrame, this will change
2862 * @sa CMainFrame::ShowTextMergeDoc()
2864 bool CMergeDoc::OpenDocs(int nFiles, const FileLocation ifileloc[],
2865 const bool bRO[], const String strDesc[])
2867 IDENTLEVEL identical = IDENTLEVEL::NONE;
2868 int nRescanResult = RESCAN_OK;
2870 FileLocation fileloc[3];
2872 std::copy_n(ifileloc, 3, fileloc);
2874 // Filter out invalid codepages, or editor will display all blank
2875 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2876 SanityCheckCodepage(fileloc[nBuffer]);
2880 curUndo = undoTgt.begin();
2882 // Prevent displaying views during LoadFile
2883 // Note : attach buffer again only if both loads succeed
2884 m_strBothFilenames.erase();
2886 ForEachView([](auto& pView) { pView->DetachFromBuffer(); });
2888 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2890 // clear undo buffers
2891 m_ptBuf[nBuffer]->m_aUndoBuf.clear();
2894 m_ptBuf[nBuffer]->FreeAll ();
2896 // build the text being filtered, "|" separates files as it is forbidden in filenames
2897 m_strBothFilenames += fileloc[nBuffer].filepath + _T("|");
2899 m_strBothFilenames.erase(m_strBothFilenames.length() - 1);
2902 DWORD nSuccess[3] = { FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR, FileLoadResult::FRESULT_ERROR };
2903 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2905 nSuccess[nBuffer] = LoadOneFile(nBuffer, fileloc[nBuffer].filepath, bRO[nBuffer], strDesc ? strDesc[nBuffer] : _T(""),
2906 fileloc[nBuffer].encoding);
2907 if (!FileLoadResult::IsOk(nSuccess[nBuffer]))
2909 CMergeEditFrame* pFrame = GetParentFrame();
2910 if (pFrame != nullptr)
2912 // Use verify macro to trap possible error in debug.
2913 VERIFY(pFrame->DestroyWindow());
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_infoUnpacker.Initialize(false);
2933 // Warn user if file load was lossy (bad encoding)
2935 int nLossyBuffers = 0;
2936 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2938 if (FileLoadResult::IsLossy(nSuccess[nBuffer]))
2940 // TODO: It would be nice to report how many lines were lossy
2941 // we did calculate those numbers when we loaded the files, in the text stats
2943 idres = IDS_LOSSY_TRANSCODING_FIRST + nBuffer;
2947 if (nLossyBuffers > 1)
2948 idres = IDS_LOSSY_TRANSCODING_BOTH; /* FIXEME */
2950 if (nLossyBuffers > 0)
2952 if (m_pEncodingErrorBar == nullptr)
2954 m_pEncodingErrorBar.reset(new CEncodingErrorBar());
2955 m_pEncodingErrorBar->Create(this->m_pView[0][0]->GetParentFrame());
2957 m_pEncodingErrorBar->SetText(LoadResString(idres));
2958 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), TRUE, FALSE);
2961 ForEachView([](auto& pView) {
2962 // Now buffers data are valid
2963 pView->AttachToBuffer();
2964 // Currently there is only one set of syntax colors, which all documents & views share
2965 pView->SetColorContext(theApp.GetMainSyntaxColors());
2966 // Currently there is only one set of markers, which all documents & views share
2967 pView->SetMarkersContext(theApp.GetMainMarkers());
2969 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
2971 // Set read-only statuses
2972 m_ptBuf[nBuffer]->SetReadOnly(bRO[nBuffer]);
2975 // Check the EOL sensitivity option (do it before Rescan)
2976 DIFFOPTIONS diffOptions = {0};
2977 m_diffWrapper.GetOptions(&diffOptions);
2978 if (!GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) && !diffOptions.bIgnoreEol)
2980 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
2981 if (m_ptBuf[0]->GetCRLFMode() != m_ptBuf[nBuffer]->GetCRLFMode())
2984 if (nBuffer < m_nBuffers)
2986 // Options and files not are not compatible :
2987 // Sensitive to EOL on, allow mixing EOL off, and files have a different EOL style.
2988 // All lines will differ, that is not very interesting and probably not wanted.
2989 // Propose to turn off the option 'sensitive to EOL'
2990 String s = theApp.LoadString(IDS_SUGGEST_IGNOREEOL);
2991 if (ShowMessageBox(s, MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_SUGGEST_IGNOREEOL) == IDYES)
2993 diffOptions.bIgnoreEol = true;
2994 m_diffWrapper.SetOptions(&diffOptions);
2996 CMessageBoxDialog dlg(nullptr, s.c_str(), _T(""), 0, IDS_SUGGEST_IGNOREEOL);
2997 const int nFormerResult = dlg.GetFormerResult();
2998 if (nFormerResult != -1)
3000 // "Don't ask this question again" checkbox is checked
3001 GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, true);
3007 // Define the prediffer
3008 PackingInfo * infoUnpacker = nullptr;
3009 PrediffingInfo * infoPrediffer = nullptr;
3010 if (bFiltersEnabled && m_pDirDoc != nullptr)
3012 m_pDirDoc->GetPluginManager().FetchPluginInfos(m_strBothFilenames, &infoUnpacker, &infoPrediffer);
3013 m_diffWrapper.SetPrediffer(infoPrediffer);
3014 m_diffWrapper.SetTextForAutomaticPrediff(m_strBothFilenames);
3017 bool bBinary = false;
3018 nRescanResult = Rescan(bBinary, identical);
3020 // Open filed if rescan succeed and files are not binaries
3021 if (nRescanResult == RESCAN_OK)
3023 // set the document types
3024 // Warning : it is the first thing to do (must be done before UpdateView,
3025 // or any function that calls UpdateView, like SelectDiff)
3026 // Note: If option enabled, and another side type is not recognized,
3027 // we use recognized type for unrecognized side too.
3032 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3034 sext[nBuffer] = GetFileExt(m_ptBuf[nBuffer]->GetTempFileName().c_str(), m_strDesc[nBuffer].c_str());
3035 ForEachView(nBuffer, [&](auto& pView) {
3036 bTyped[nBuffer] = pView->SetTextType(sext[nBuffer].c_str());
3037 if (bTyped[nBuffer])
3038 paneTyped = nBuffer;
3042 for (nBuffer = 1; nBuffer < m_nBuffers; nBuffer++)
3044 if (bTyped[0] != bTyped[nBuffer])
3048 bool syntaxHLEnabled = GetOptionsMgr()->GetBool(OPT_SYNTAX_HIGHLIGHT);
3049 if (syntaxHLEnabled && nBuffer < m_nBuffers)
3051 if (std::count(bTyped, bTyped + m_nBuffers, false) == m_nBuffers)
3054 m_ptBuf[0]->GetLine(0, sFirstLine);
3055 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3057 bTyped[nBuffer] = GetView(0, nBuffer)->SetTextTypeByContent(sFirstLine);
3062 if (syntaxHLEnabled)
3064 CrystalLineParser::TextDefinition *enuType = CrystalLineParser::GetTextType(sext[paneTyped].c_str());
3065 ForEachView([&bTyped, enuType](auto& pView) {
3066 if (!bTyped[pView->m_nThisPane])
3067 pView->SetTextType(enuType);
3071 int nNormalBuffer = 0;
3072 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3074 // set the frame window header
3075 UpdateHeaderPath(nBuffer);
3077 ForEachView(nBuffer, [](auto& pView) { pView->DocumentsLoaded(); });
3079 if ((m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL) ||
3080 (m_nBufferType[nBuffer] == BUFFERTYPE::NORMAL_NAMED))
3087 // Inform user that files are identical
3088 // Don't show message if new buffers created
3089 if (identical == IDENTLEVEL::ALL && nNormalBuffer > 0)
3091 ShowRescanError(nRescanResult, identical);
3094 // Exit if files are identical should only work for the first
3095 // comparison and must be disabled afterward.
3096 theApp.m_bExitIfNoDiff = MergeCmdLineInfo::Disabled;
3100 // CMergeDoc::Rescan fails if files do not exist on both sides
3101 // or the really arcane case that the temp files couldn't be created,
3102 // which is too obscure to bother reporting if you can't write to
3103 // your temp directory, doing nothing is graceful enough for that).
3104 ShowRescanError(nRescanResult, identical);
3105 GetParentFrame()->DestroyWindow();
3109 // Force repaint of location pane to update it in case we had some warning
3110 // dialog visible and it got painted before files were loaded
3111 if (m_pView[0][0] != nullptr)
3112 m_pView[0][0]->RepaintLocationPane();
3117 void CMergeDoc::MoveOnLoad(int nPane, int nLineIndex, bool bRealLine)
3121 nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
3122 if (nPane < 0 || nPane >= m_nBuffers)
3125 if (nLineIndex == -1)
3127 // scroll to first diff
3128 if (GetOptionsMgr()->GetBool(OPT_SCROLL_TO_FIRST) &&
3129 m_diffList.HasSignificantDiffs())
3131 int nDiff = m_diffList.FirstSignificantDiff();
3133 m_pView[0][nPane]->SelectDiff(nDiff, true, false);
3134 m_pView[0][nPane]->SetActivePane();
3138 m_pView[0][nPane]->GotoLine(nLineIndex < 0 ? 0 : nLineIndex, bRealLine, nPane);
3141 void CMergeDoc::ChangeFile(int nBuffer, const String& path, int nLineIndex)
3143 if (!PromptAndSaveIfNeeded(true))
3146 FileLocation fileloc[3];
3149 for (int pane = 0; pane < m_nBuffers; pane++)
3151 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3152 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3153 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3154 fileloc[pane].setPath(m_filePaths[pane]);
3156 std::copy_n(m_strDesc, m_nBuffers, strDesc);
3158 strDesc[nBuffer] = _T("");
3159 fileloc[nBuffer].setPath(path);
3160 fileloc[nBuffer].encoding = codepage_detect::Guess(path, GetOptionsMgr()->GetInt(OPT_CP_DETECT));
3162 if (OpenDocs(m_nBuffers, fileloc, bRO, strDesc))
3163 MoveOnLoad(nBuffer, nLineIndex);
3167 * @brief Re-load a document.
3168 * This methods re-loads the file compare document. The re-loaded document is
3169 * one side of the file compare.
3170 * @param [in] index The document to re-load.
3171 * @return Open result code.
3173 void CMergeDoc::RefreshOptions()
3175 DIFFOPTIONS options = {0};
3177 m_bAutomaticRescan = GetOptionsMgr()->GetBool(OPT_AUTOMATIC_RESCAN);
3179 m_diffWrapper.SetDetectMovedBlocks(GetOptionsMgr()->GetBool(OPT_CMP_MOVED_BLOCKS));
3180 Options::DiffOptions::Load(GetOptionsMgr(), options);
3182 m_diffWrapper.SetOptions(&options);
3184 // Refresh view options
3185 ForEachView([](auto& pView) { pView->RefreshOptions(); });
3189 * @brief Write path and filename to headerbar
3190 * @note SetText() does not repaint unchanged text
3192 void CMergeDoc::UpdateHeaderPath(int pane)
3194 CMergeEditFrame *pf = GetParentFrame();
3195 ASSERT(pf != nullptr);
3197 bool bChanges = false;
3199 if (m_nBufferType[pane] == BUFFERTYPE::UNNAMED ||
3200 m_nBufferType[pane] == BUFFERTYPE::NORMAL_NAMED)
3202 sText = m_strDesc[pane];
3206 sText = m_filePaths[pane];
3207 if (m_pDirDoc != nullptr)
3209 m_pDirDoc->ApplyDisplayRoot(pane, sText);
3212 bChanges = m_ptBuf[pane]->IsModified();
3215 sText.insert(0, _T("* "));
3217 pf->GetHeaderInterface()->SetText(pane, sText);
3223 * @brief Paint differently the headerbar of the active view
3225 void CMergeDoc::UpdateHeaderActivity(int pane, bool bActivate)
3227 CMergeEditFrame *pf = GetParentFrame();
3228 ASSERT(pf != nullptr);
3229 pf->GetHeaderInterface()->SetActive(pane, bActivate);
3233 * @brief Set detect/not detect Moved Blocks
3235 void CMergeDoc::SetDetectMovedBlocks(bool bDetectMovedBlocks)
3237 if (bDetectMovedBlocks == m_diffWrapper.GetDetectMovedBlocks())
3240 GetOptionsMgr()->SaveOption(OPT_CMP_MOVED_BLOCKS, bDetectMovedBlocks);
3241 m_diffWrapper.SetDetectMovedBlocks(bDetectMovedBlocks);
3246 * @brief Check if given buffer has mixed EOL style.
3247 * @param [in] nBuffer Buffer to check.
3248 * @return true if buffer's EOL style is mixed, false otherwise.
3250 bool CMergeDoc::IsMixedEOL(int nBuffer) const
3252 CDiffTextBuffer *pBuf = m_ptBuf[nBuffer].get();
3253 return pBuf->IsMixedEOL();
3256 void CMergeDoc::SetEditedAfterRescan(int nBuffer)
3258 m_bEditAfterRescan[nBuffer] = true;
3261 bool CMergeDoc::IsEditedAfterRescan(int nBuffer) const
3263 if (nBuffer >= 0 && nBuffer < m_nBuffers)
3264 return m_bEditAfterRescan[nBuffer];
3266 for (nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3268 if (m_bEditAfterRescan[nBuffer])
3276 * @brief Update document filenames to title
3278 void CMergeDoc::SetTitle(LPCTSTR lpszTitle)
3280 PrediffingInfo infoPrediffer;
3281 GetPrediffer(&infoPrediffer);
3282 String sTitle = (lpszTitle != nullptr) ? lpszTitle : CMergeFrameCommon::GetTitleString(m_filePaths, m_strDesc, &m_infoUnpacker, &infoPrediffer);
3283 CDocument::SetTitle(sTitle.c_str());
3287 * @brief Update any resources necessary after a GUI language change
3289 void CMergeDoc::UpdateResources()
3291 if (m_nBufferType[0] == BUFFERTYPE::UNNAMED)
3292 m_strDesc[0] = _("Untitled left");
3293 if (m_nBufferType[m_nBuffers - 1] == BUFFERTYPE::UNNAMED)
3294 m_strDesc[m_nBuffers - 1] = _("Untitled right");
3295 if (m_nBuffers == 3 && m_nBufferType[1] == BUFFERTYPE::UNNAMED)
3296 m_strDesc[1] = _("Untitled middle");
3297 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3298 UpdateHeaderPath(nBuffer);
3300 GetParentFrame()->UpdateResources();
3301 ForEachView([](auto& pView) { pView->UpdateResources(); });
3304 // Return current word breaking break type setting (whitespace only or include punctuation)
3305 bool CMergeDoc::GetBreakType() const
3307 bool breakType = !!GetOptionsMgr()->GetInt(OPT_BREAK_TYPE);
3311 // Return true to do line diff colors at the byte level (false to do them at word level)
3312 bool CMergeDoc::GetByteColoringOption() const
3314 // color at byte level if 'break_on_words' option not set
3315 bool breakWords = GetOptionsMgr()->GetBool(OPT_BREAK_ON_WORDS);
3319 /// Swap files and update views
3320 void CMergeDoc::SwapFiles(int nFromIndex, int nToIndex)
3322 if ((nFromIndex >= 0 && nFromIndex < m_nBuffers) && (nToIndex >= 0 && nToIndex < m_nBuffers))
3325 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3327 int nLeftViewId = m_pView[nGroup][nFromIndex]->GetDlgCtrlID();
3328 int nRightViewId = m_pView[nGroup][nToIndex]->GetDlgCtrlID();
3329 m_pView[nGroup][nFromIndex]->SetDlgCtrlID(nRightViewId);
3330 m_pView[nGroup][nToIndex]->SetDlgCtrlID(nLeftViewId);
3334 // Swap buffers and so on
3335 std::swap(m_ptBuf[nFromIndex], m_ptBuf[nToIndex]);
3336 for (int nGroup = 0; nGroup < m_nGroups; ++nGroup)
3337 std::swap(m_pView[nGroup][nFromIndex], m_pView[nGroup][nToIndex]);
3338 std::swap(m_pSaveFileInfo[nFromIndex], m_pSaveFileInfo[nToIndex]);
3339 std::swap(m_pRescanFileInfo[nFromIndex], m_pRescanFileInfo[nToIndex]);
3340 std::swap(m_nBufferType[nFromIndex], m_nBufferType[nToIndex]);
3341 std::swap(m_bEditAfterRescan[nFromIndex], m_bEditAfterRescan[nToIndex]);
3342 std::swap(m_strDesc[nFromIndex], m_strDesc[nToIndex]);
3344 m_filePaths.Swap(nFromIndex, nToIndex);
3345 m_diffList.Swap(nFromIndex, nToIndex);
3346 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3347 swap(m_pView[nGroup][nFromIndex]->m_piMergeEditStatus, m_pView[nGroup][nToIndex]->m_piMergeEditStatus);
3349 ClearWordDiffCache();
3351 for (int nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3353 m_ptBuf[nBuffer]->m_nThisPane = nBuffer;
3354 for (int nGroup = 0; nGroup < m_nGroups; nGroup++)
3355 m_pView[nGroup][nBuffer]->m_nThisPane = nBuffer;
3358 UpdateHeaderPath(nBuffer);
3360 GetParentFrame()->UpdateSplitter();
3361 ForEachView([](auto& pView) { pView->UpdateStatusbar(); });
3363 UpdateAllViews(nullptr);
3368 * @brief Reloads the opened files
3370 void CMergeDoc::OnFileReload()
3372 if (!PromptAndSaveIfNeeded(true))
3375 FileLocation fileloc[3];
3377 for (int pane = 0; pane < m_nBuffers; pane++)
3379 bRO[pane] = m_ptBuf[pane]->GetReadOnly();
3380 fileloc[pane].encoding.m_unicoding = m_ptBuf[pane]->getUnicoding();
3381 fileloc[pane].encoding.m_codepage = m_ptBuf[pane]->getCodepage();
3382 fileloc[pane].setPath(m_filePaths[pane]);
3384 CPoint pt = GetActiveMergeView()->GetCursorPos();
3385 if (OpenDocs(m_nBuffers, fileloc, bRO, m_strDesc))
3386 MoveOnLoad(GetActiveMergeView()->m_nThisPane, pt.y);
3390 * @brief Display encodings to user
3392 void CMergeDoc::OnFileEncoding()
3394 DoFileEncodingDialog();
3397 void CMergeDoc::OnOpenWithUnpacker()
3399 CSelectPluginDlg dlg(m_infoUnpacker.GetPluginPipeline(),
3400 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3401 CSelectPluginDlg::PluginType::Unpacker, false);
3402 if (dlg.DoModal() != IDOK)
3405 if (!PromptAndSaveIfNeeded(true))
3408 PackingInfo infoUnpacker(dlg.GetPluginPipeline());
3409 PathContext paths = m_filePaths;
3410 DWORD dwFlags[3] = { FFILEOPEN_NOMRU, FFILEOPEN_NOMRU, FFILEOPEN_NOMRU };
3411 String strDesc[3] = { m_strDesc[0], m_strDesc[1], m_strDesc[2] };
3412 int nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3413 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3415 if (GetMainFrame()->DoFileOpen(nID, &paths, dwFlags, strDesc, _T(""), &infoUnpacker))
3416 GetParentFrame()->DestroyWindow();
3419 void CMergeDoc::OnApplyPrediffer()
3421 PrediffingInfo prediffer;
3422 GetPrediffer(&prediffer);
3423 // let the user choose a handler
3424 CSelectPluginDlg dlg(prediffer.GetPluginPipeline(),
3425 strutils::join(m_filePaths.begin(), m_filePaths.end(), _T("|")),
3426 CSelectPluginDlg::PluginType::Prediffer, false);
3427 if (dlg.DoModal() != IDOK)
3429 prediffer.SetPluginPipeline(dlg.GetPluginPipeline());
3430 SetPrediffer(&prediffer);
3431 m_CurrentPredifferID = -1;
3432 FlushAndRescan(true);
3437 * @brief Create the dynamic submenu for prediffers
3439 * @note The plugins are grouped in (suggested) and (not suggested)
3440 * The IDs follow the order of GetAvailableScripts
3442 * suggested 0 ID_1ST + 0
3443 * suggested 1 ID_1ST + 2
3444 * suggested 2 ID_1ST + 5
3445 * not suggested 0 ID_1ST + 1
3446 * not suggested 1 ID_1ST + 3
3447 * not suggested 2 ID_1ST + 4
3449 HMENU CMergeDoc::createPrediffersSubmenu(HMENU hMenu)
3452 int j = GetMenuItemCount(hMenu);
3454 DeleteMenu(hMenu, 0, MF_BYPOSITION);
3457 AppendMenu(hMenu, MF_STRING, ID_NO_PREDIFFER, _("No prediffer (normal)").c_str());
3459 if (!GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED))
3462 m_CurrentPredifferID = -1;
3464 // compute the m_CurrentPredifferID (to set the radio button)
3465 PrediffingInfo prediffer;
3466 GetPrediffer(&prediffer);
3467 if (prediffer.GetPluginPipeline().empty())
3468 m_CurrentPredifferID = ID_NO_PREDIFFER;
3470 // get the scriptlet files
3471 const auto& [ suggestedPlugins, allPlugins ]= FileTransform::CreatePluginMenuInfos(
3472 m_strBothFilenames, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3474 // build the menu : first part, suggested plugins
3476 AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3477 AppendMenu(hMenu, MF_STRING, ID_SUGGESTED_PLUGINS, _("Suggested plugins").c_str());
3479 for (const auto& [caption, name, id, plugin ] : suggestedPlugins)
3480 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3482 // build the menu : second part, others plugins
3484 AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
3485 AppendMenu(hMenu, MF_STRING, ID_NOT_SUGGESTED_PLUGINS, _("Other plugins").c_str());
3487 String lastPluginName;
3488 String errorMessage;
3489 auto result = prediffer.ParsePluginPipeline(errorMessage);
3490 if (result.size() > 0)
3491 lastPluginName = result.back().name;
3493 for (const auto& [processType, pluginAry] : allPlugins)
3495 for (const auto& [caption, name, id, plugin] : pluginAry)
3499 AppendMenu(hMenu, MF_STRING, id, caption.c_str());
3500 if (lastPluginName == plugin->m_name)
3501 m_CurrentPredifferID = id;
3510 * @brief Called when an editor script item is updated
3512 void CMergeDoc::OnUpdatePrediffer(CCmdUI* pCmdUI)
3514 pCmdUI->Enable(true);
3516 PrediffingInfo prediffer;
3517 GetPrediffer(&prediffer);
3519 if (prediffer.GetPluginPipeline().find(_T("<Automatic>")) != String::npos)
3521 pCmdUI->SetRadio(false);
3525 // Detect when CDiffWrapper::RunFileDiff has canceled a buggy prediffer
3526 if (prediffer.GetPluginPipeline().empty())
3527 m_CurrentPredifferID = ID_NO_PREDIFFER;
3529 pCmdUI->SetRadio(pCmdUI->m_nID == static_cast<UINT>(m_CurrentPredifferID));
3533 * @brief Handler for all prediffer choices, including ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO, ID_NO_PREDIFFER, & specific prediffers
3535 void CMergeDoc::OnPrediffer(UINT nID )
3537 SetPredifferByMenu(nID);
3538 FlushAndRescan(true);
3543 * @brief Handler for all prediffer choices.
3544 * Prediffer choises include ID_PREDIFF_MANUAL, ID_PREDIFF_AUTO,
3545 * ID_NO_PREDIFFER, & specific prediffers.
3547 void CMergeDoc::SetPredifferByMenu(UINT nID)
3549 // update data for the radio button
3550 m_CurrentPredifferID = nID;
3552 if (nID == ID_NO_PREDIFFER)
3554 // All flags are set correctly during the construction
3555 PrediffingInfo infoPrediffer(false);
3556 SetPrediffer(&infoPrediffer);
3560 String pluginName = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::PredifferEventNames, ID_PREDIFFERS_FIRST);
3562 // build a PrediffingInfo structure fom the ID
3563 PrediffingInfo prediffer(pluginName);
3565 // update the prediffer and rescan
3566 SetPrediffer(&prediffer);
3569 void CMergeDoc::OnBnClickedFileEncoding()
3571 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3573 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3574 DoFileEncodingDialog();
3577 void CMergeDoc::OnBnClickedPlugin()
3579 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3581 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3582 OnOpenWithUnpacker();
3585 void CMergeDoc::OnBnClickedHexView()
3587 OnFileRecompareAs(ID_MERGE_COMPARE_HEX);
3590 void CMergeDoc::OnOK()
3592 if (m_pEncodingErrorBar == nullptr || m_pView[0][0] == nullptr)
3594 m_pView[0][0]->GetParentFrame()->ShowControlBar(m_pEncodingErrorBar.get(), FALSE, FALSE);
3597 void CMergeDoc::OnFileRecompareAsText()
3599 m_bEnableTableEditing = false;
3603 void CMergeDoc::OnUpdateFileRecompareAsText(CCmdUI *pCmdUI)
3605 pCmdUI->Enable(m_ptBuf[0]->GetTableEditing());
3608 void CMergeDoc::OnFileRecompareAsTable()
3610 m_bEnableTableEditing = true;
3614 void CMergeDoc::OnUpdateFileRecompareAsTable(CCmdUI *pCmdUI)
3616 pCmdUI->Enable(!m_ptBuf[0]->GetTableEditing());
3619 void CMergeDoc::OnFileRecompareAs(UINT nID)
3621 if (!PromptAndSaveIfNeeded(true))
3624 DWORD dwFlags[3] = { 0 };
3625 FileLocation fileloc[3];
3627 int nBuffers = m_nBuffers;
3628 CDirDoc *pDirDoc = m_pDirDoc->GetMainView() ? m_pDirDoc :
3629 static_cast<CDirDoc*>(theApp.m_pDirTemplate->CreateNewDocument());
3630 PackingInfo infoUnpacker(m_infoUnpacker.GetPluginPipeline());
3632 for (int pane = 0; pane < m_nBuffers; pane++)
3634 fileloc[pane].setPath(m_filePaths[pane]);
3635 dwFlags[pane] |= FFILEOPEN_NOMRU | (m_ptBuf[pane]->GetReadOnly() ? FFILEOPEN_READONLY : 0);
3636 strDesc[pane] = m_strDesc[pane];
3638 if (ID_UNPACKERS_FIRST <= nID && nID <= ID_UNPACKERS_LAST)
3640 infoUnpacker.SetPluginPipeline(CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST));
3641 nID = m_ptBuf[0]->GetTableEditing() ? ID_MERGE_COMPARE_TABLE : ID_MERGE_COMPARE_TEXT;
3642 nID = GetOptionsMgr()->GetBool(OPT_PLUGINS_OPEN_IN_SAME_FRAME_TYPE) ? nID : -1;
3645 if (GetMainFrame()->ShowMergeDoc(nID, pDirDoc, nBuffers, fileloc, dwFlags, strDesc, _T(""), &infoUnpacker))
3646 GetParentFrame()->DestroyWindow();
3649 // Return file extension either from file name
3650 String CMergeDoc::GetFileExt(LPCTSTR sFileName, LPCTSTR sDescription) const
3653 paths::SplitFilename(sFileName, nullptr, nullptr, &sExt);
3658 * @brief Generate report from file compare results.
3660 bool CMergeDoc::GenerateReport(const String& sFileName) const
3662 // calculate HTML font size
3665 dc.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);
3666 m_pView[0][0]->GetFont(lf);
3667 int nFontSize = -MulDiv (lf.lfHeight, 72, dc.GetDeviceCaps (LOGPIXELSY));
3669 // create HTML report
3671 if (!file.Open(sFileName, _T("wt")))
3673 String errMsg = GetSysError(GetLastError());
3674 String msg = strutils::format_string1(
3675 _("Error creating the report:\n%1"), errMsg);
3676 AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
3680 file.SetCodepage(ucr::CP_UTF_8);
3682 CString headerText =
3683 _T("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n")
3684 _T("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n")
3687 _T("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n")
3688 _T("<title>WinMerge File Compare Report</title>\n")
3689 _T("<style type=\"text/css\">\n")
3691 _T("table {margin: 0; border: 1px solid #a0a0a0; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15);}\n")
3692 _T("td,th {word-break: break-all; font-size: %dpt;padding: 0 3px;}\n")
3693 _T("tr { vertical-align: top; }\n")
3694 _T(".title {color: white; background-color: blue; vertical-align: top; padding: 4px 4px; background: linear-gradient(mediumblue, darkblue);}\n")
3700 _T("<table cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%%;\">\n")
3704 strutils::format((LPCTSTR)headerText, nFontSize, (LPCTSTR)m_pView[0][0]->GetHTMLStyles());
3705 file.WriteString(header);
3708 // If archive, use archive path + folder + filename inside archive
3709 // If desc text given, use it
3710 PathContext paths = m_filePaths;
3711 if (m_pDirDoc != nullptr && m_pDirDoc->IsArchiveFolders())
3713 for (int i = 0; i < paths.GetSize(); i++)
3714 m_pDirDoc->ApplyDisplayRoot(i, paths[i]);
3718 for (int i = 0; i < paths.GetSize(); i++)
3720 if (!m_strDesc[i].empty())
3721 paths[i] = m_strDesc[i];
3727 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3729 String data = strutils::format(_T("<th colspan=\"2\" class=\"title\" style=\"width:%f%%\">"),
3730 (double)100 / m_nBuffers);
3731 file.WriteString(data);
3732 file.WriteString(ucr::toTString(CMarkdown::Entities(ucr::toUTF8(paths[nBuffer]))));
3733 file.WriteString(_T("</th>\n"));
3740 // write the body of the report
3742 int nLineCount[3] = {0};
3744 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3745 nLineCount[nBuffer] = m_ptBuf[nBuffer]->GetLineCount();
3749 file.WriteString(_T("<tr>\n"));
3750 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3752 for (; idx[nBuffer] < nLineCount[nBuffer]; idx[nBuffer]++)
3754 if (m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3758 if (idx[nBuffer] < nLineCount[nBuffer])
3761 int iVisibleLineNumber = 0;
3762 String tdtag = _T("<td class=\"ln\">");
3763 DWORD dwFlags = m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer]);
3764 if ((dwFlags & LF_GHOST) == 0 && m_pView[0][nBuffer]->GetViewLineNumbers())
3766 iVisibleLineNumber = m_ptBuf[nBuffer]->ComputeRealLine(idx[nBuffer]) + 1;
3769 (dwFlags & (LF_DIFF | LF_GHOST)) != 0 && (idx[nBuffer] == 0 ||
3770 (m_ptBuf[nBuffer]->GetLineFlags(idx[nBuffer] - 1) & (LF_DIFF | LF_GHOST)) == 0))
3773 if (iVisibleLineNumber > 0)
3775 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">%d</a>"), nDiff, nDiff, iVisibleLineNumber);
3776 iVisibleLineNumber = 0;
3779 tdtag += strutils::format(_T("<a name=\"d%d\" href=\"#d%d\">.</a>"), nDiff, nDiff);
3781 if (iVisibleLineNumber > 0)
3782 tdtag += strutils::format(_T("%d</td>"), iVisibleLineNumber);
3784 tdtag += _T("</td>");
3785 file.WriteString(tdtag);
3787 file.WriteString((LPCTSTR)m_pView[0][nBuffer]->GetHTMLLine(idx[nBuffer], _T("td")));
3791 file.WriteString(_T("<td class=\"ln\"></td><td></td>"));
3792 file.WriteString(_T("\n"));
3794 file.WriteString(_T("</tr>\n"));
3796 bool bBorderLine = false;
3797 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3799 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3805 file.WriteString(_T("<tr height=1>"));
3806 for (nBuffer = 0; nBuffer < m_nBuffers; nBuffer++)
3808 if (idx[nBuffer] < nLineCount[nBuffer] && !m_pView[0][nBuffer]->GetLineVisible(idx[nBuffer]))
3809 file.WriteString(_T("<td style=\"background-color: black\"></td><td style=\"background-color: black\"></td>"));
3811 file.WriteString(_T("<td></td><td></td>"));
3813 file.WriteString(_T("</tr>\n"));
3816 if (idx[0] >= nLineCount[0] && idx[1] >= nLineCount[1] && (m_nBuffers < 3 || idx[2] >= nLineCount[2]))
3831 * @brief Generate report from file compare results.
3833 void CMergeDoc::OnToolsGenerateReport()
3838 if (!SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, folder, _T(""), _("HTML Files (*.htm,*.html)|*.htm;*.html|All Files (*.*)|*.*||"), _T("htm")))
3841 if (GenerateReport(s.c_str()))
3842 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
3846 * @brief Generate patch from files selected.
3848 * Creates a patch from selected files in active directory compare, or
3849 * active file compare. Files in file compare must be saved before
3852 void CMergeDoc::OnToolsGeneratePatch()
3854 // If there are changes in files, tell user to save them first
3857 LangMessageBox(IDS_SAVEFILES_FORPATCH, MB_ICONSTOP);
3862 patcher.AddFiles(m_filePaths.GetLeft(),
3863 m_filePaths.GetRight());
3864 patcher.CreatePatch();
3868 * @brief Add synchronization point
3870 void CMergeDoc::AddSyncPoint()
3873 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3875 int tmp = m_pView[0][nBuffer]->GetCursorPos().y;
3876 nLine[nBuffer] = m_ptBuf[nBuffer]->ComputeApparentLine(m_ptBuf[nBuffer]->ComputeRealLine(tmp));
3879 // If adding a sync point by selecting a ghost line that is after the last block, Cancel the process adding a sync point.
3880 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3881 if (nLine[nBuffer] >= m_ptBuf[nBuffer]->GetLineCount())
3883 LangMessageBox(IDS_SYNCPOINT_LASTBLOCK, MB_ICONSTOP);
3887 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3888 if (m_ptBuf[nBuffer]->GetLineFlags(nLine[nBuffer]) & LF_INVALID_BREAKPOINT)
3889 DeleteSyncPoint(nBuffer, nLine[nBuffer], false);
3891 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3892 m_ptBuf[nBuffer]->SetLineFlag(nLine[nBuffer], LF_INVALID_BREAKPOINT, true, false);
3894 m_bHasSyncPoints = true;
3896 ForEachView([](auto& pView) { pView->SetSelectionMargin(true); });
3898 FlushAndRescan(true);
3902 * @brief Delete a synchronization point
3904 bool CMergeDoc::DeleteSyncPoint(int pane, int nLine, bool bRescan)
3906 const auto syncpoints = GetSyncPointList();
3907 for (auto syncpnt : syncpoints)
3909 if (syncpnt[pane] == nLine)
3911 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3912 m_ptBuf[nBuffer]->SetLineFlag(syncpnt[nBuffer], LF_INVALID_BREAKPOINT, false, false);
3916 if (syncpoints.size() == 1)
3917 m_bHasSyncPoints = false;
3920 FlushAndRescan(true);
3925 * @brief Clear Synchronization points
3927 void CMergeDoc::ClearSyncPoints()
3929 if (!m_bHasSyncPoints)
3932 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3934 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3935 for (int nLine = 0; nLine < nLineCount; ++nLine)
3937 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3938 m_ptBuf[nBuffer]->SetLineFlag(nLine, LF_INVALID_BREAKPOINT, false, false);
3942 m_bHasSyncPoints = false;
3944 FlushAndRescan(true);
3947 std::vector<std::vector<int> > CMergeDoc::GetSyncPointList()
3949 std::vector<std::vector<int> > list;
3950 if (!m_bHasSyncPoints)
3952 int idx[3] = {-1, -1, -1};
3953 std::vector<int> points(m_nBuffers);
3954 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3955 points[nBuffer] = m_ptBuf[nBuffer]->GetLineCount() - 1;
3956 for (int nBuffer = 0; nBuffer < m_nBuffers; ++nBuffer)
3958 int nLineCount = m_ptBuf[nBuffer]->GetLineCount();
3959 for (int nLine = 0; nLine < nLineCount; ++nLine)
3961 if (m_ptBuf[nBuffer]->GetLineFlags(nLine) & LF_INVALID_BREAKPOINT)
3964 if (static_cast<int>(list.size()) <= idx[nBuffer])
3965 list.push_back(points);
3966 list[idx[nBuffer]][nBuffer] = nLine;