OSDN Git Service

Add Expand Different Subfolders menu item (#1964)
[winmerge-jp/winmerge-jp.git] / Src / DiffTextBuffer.cpp
1 /** 
2  * @file  DiffTextBuffer.cpp
3  *
4  * @brief Implementation file for CDiffTextBuffer
5  *
6  */
7
8 #include "StdAfx.h"
9 #include "DiffTextBuffer.h"
10 #include "ccrystaltextview.h"
11 #include "UniFile.h"
12 #include "FileLoadResult.h"
13 #include "locality.h"
14 #include "paths.h"
15 #include "OptionsDef.h"
16 #include "OptionsMgr.h"
17 #include "Environment.h"
18 #include "MergeLineFlags.h"
19 #include "MergeDoc.h"
20 #include "FileTransform.h"
21 #include "FileTextEncoding.h"
22 #include "codepage_detect.h"
23 #include "TFile.h"
24 #include <Poco/Exception.h>
25
26 using Poco::Exception;
27
28 #ifdef _DEBUG
29 #define new DEBUG_NEW
30 #endif
31
32 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats);
33 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats);
34
35 /**
36  * @brief Check if file has only one EOL type.
37  * @param [in] stats File's text stats.
38  * @return true if only one EOL type is found, false otherwise.
39  */
40 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats)
41 {
42         int nType = 0;
43         if (stats.ncrlfs > 0)
44                 nType++;
45         if ( stats.ncrs > 0)
46                 nType++;
47         if (stats.nlfs > 0)
48                 nType++;
49         return (nType <= 1);
50 }
51
52 /**
53  * @brief Get file's EOL type.
54  * @param [in] stats File's text stats.
55  * @return EOL type.
56  */
57 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats)
58 {
59         // Check if file has more than one EOL type.
60         if (!IsTextFileStylePure(stats))
61                         return CRLFSTYLE::MIXED;
62         else if (stats.ncrlfs >= stats.nlfs)
63         {
64                 if (stats.ncrlfs >= stats.ncrs)
65                         return CRLFSTYLE::DOS;
66                 else
67                         return CRLFSTYLE::MAC;
68         }
69         else
70         {
71                 if (stats.nlfs >= stats.ncrs)
72                         return CRLFSTYLE::UNIX;
73                 else
74                         return CRLFSTYLE::MAC;
75         }
76 }
77
78 /**
79  * @brief Constructor.
80  * @param [in] pDoc Owning CMergeDoc.
81  * @param [in] pane Pane number this buffer is associated with.
82  */
83 CDiffTextBuffer::CDiffTextBuffer(CMergeDoc * pDoc, int pane)
84 : m_pOwnerDoc(pDoc)
85 , m_nThisPane(pane)
86 , m_bMixedEOL(false)
87 {
88 }
89
90 /**
91  * @brief Get a line from the buffer.
92  * @param [in] nLineIndex Index of the line to get.
93  * @param [out] strLine Returns line text in the index.
94  */
95 bool CDiffTextBuffer::GetLine(int nLineIndex, String &strLine) const
96 {
97         int nLineLength = CCrystalTextBuffer::GetLineLength(nLineIndex);
98         if (nLineLength < 0)
99                 return false;
100         else if (nLineLength == 0)
101                 strLine.clear();
102         else
103                 strLine.assign(CCrystalTextBuffer::GetLineChars(nLineIndex), nLineLength);
104         return true;
105 }
106
107 /**
108  * @brief Set the buffer modified status.
109  * @param [in] bModified New modified status, true if buffer has been
110  *   modified since last saving.
111  */
112 void CDiffTextBuffer::                  /* virtual override */
113 SetModified(bool bModified /*= true*/)  
114 {
115         CCrystalTextBuffer::SetModified (bModified);
116         m_pOwnerDoc->SetModifiedFlag (bModified);
117 }
118
119 void CDiffTextBuffer::                  /* virtual override */
120 AddUndoRecord(bool bInsert, const CEPoint & ptStartPos,
121                 const CEPoint & ptEndPos, const tchar_t* pszText, size_t cchText,
122                 int nActionType /*= CE_ACTION_UNKNOWN*/,
123                 std::vector<uint32_t> *paSavedRevisionNumbers /*= nullptr*/)
124 {
125         CGhostTextBuffer::AddUndoRecord(bInsert, ptStartPos, ptEndPos, pszText,
126                 cchText, nActionType, paSavedRevisionNumbers);
127         if (m_aUndoBuf[m_nUndoPosition - 1].m_dwFlags & UNDO_BEGINGROUP)
128         {
129                 m_pOwnerDoc->undoTgt.erase(m_pOwnerDoc->curUndo, m_pOwnerDoc->undoTgt.end());
130                 m_pOwnerDoc->undoTgt.push_back(m_nThisPane);
131                 m_pOwnerDoc->curUndo = m_pOwnerDoc->undoTgt.end();
132         }
133 }
134
135 /**
136  * @brief Checks if a flag is set for line.
137  * @param [in] line Index (0-based) for line.
138  * @param [in] flag Flag to check.
139  * @return true if flag is set, false otherwise.
140  */
141 bool CDiffTextBuffer::FlagIsSet(int line, lineflags_t flag) const
142 {
143         return ((m_aLines[line].m_dwFlags & flag) == flag);
144 }
145
146 /**
147 Remove blank lines and clear winmerge flags
148 (2003-06-21, Perry: I don't understand why this is necessary, but if this isn't 
149 done, more and more gray lines appear in the file)
150 (2003-07-31, Laoran I don't understand either why it is necessary, but it works
151 fine, so let's go on with it)
152 */
153 void CDiffTextBuffer::prepareForRescan()
154 {
155         RemoveAllGhostLines();
156         for (int ct = GetLineCount() - 1; ct >= 0; --ct)
157         {
158                 SetLineFlag(ct, 
159                         LF_INVISIBLE | LF_DIFF | LF_TRIVIAL | LF_MOVED | LF_SNP,
160                         false, false, false);
161         }
162 }
163
164 /** 
165  * @brief Called when line has been edited.
166  * After editing a line, we don't know if there is a diff or not.
167  * So we clear the LF_DIFF flag (and it is more easy to read during editing).
168  * Rescan will set the proper color.
169  * @param [in] nLine Line that has been edited.
170  */
171
172 void CDiffTextBuffer::                  /* virtual override */
173 OnNotifyLineHasBeenEdited(int nLine)
174 {
175         SetLineFlag(nLine, LF_DIFF, false, false, false);
176         SetLineFlag(nLine, LF_TRIVIAL, false, false, false);
177         SetLineFlag(nLine, LF_MOVED, false, false, false);
178         SetLineFlag(nLine, LF_SNP, false, false, false);
179         CGhostTextBuffer::OnNotifyLineHasBeenEdited(nLine);
180 }
181
182 /**
183  * @brief Load file from disk into buffer
184  *
185  * @param [in] pszFileNameInit File to load
186  * @param [in] infoUnpacker Unpacker plugin
187  * @param [in] sToFindUnpacker String for finding unpacker plugin
188  * @param [out] readOnly Loading was lossy so file should be read-only
189  * @param [in] nCrlfStyle EOL style used
190  * @param [in] encoding Encoding used
191  * @param [out] sError Error message returned
192  * @return FRESULT_OK when loading succeed or (list in files.h):
193  * - FRESULT_OK_IMPURE : load OK, but the EOL are of different types
194  * - FRESULT_ERROR_UNPACK : plugin failed to unpack
195  * - FRESULT_ERROR : loading failed, sError contains error message
196  * - FRESULT_BINARY : file is binary file
197  * @note If this method fails, it calls InitNew so the CDiffTextBuffer is in a valid state
198  */
199 int CDiffTextBuffer::LoadFromFile(const tchar_t* pszFileNameInit,
200                 PackingInfo& infoUnpacker, const tchar_t* sToFindUnpacker, bool & readOnly,
201                 CRLFSTYLE nCrlfStyle, const FileTextEncoding & encoding, String &sError)
202 {
203         ASSERT(!m_bInit);
204         ASSERT(m_aLines.size() == 0);
205
206         // Unpacking the file here, save the result in a temporary file
207         m_strTempFileName = pszFileNameInit;
208         if (!infoUnpacker.Unpacking(&m_unpackerSubcodes, m_strTempFileName, sToFindUnpacker, { m_strTempFileName }))
209         {
210                 InitNew(); // leave crystal editor in valid, empty state
211                 return FileLoadResult::FRESULT_ERROR_UNPACK;
212         }
213
214         // we will load the transformed file
215         const tchar_t* pszFileName = m_strTempFileName.c_str();
216
217         String sExt;
218         FileLoadResult::flags_t nRetVal = FileLoadResult::FRESULT_OK;
219
220         // Set encoding based on extension, if we know one
221         paths::SplitFilename(pszFileName, nullptr, nullptr, &sExt);
222         CrystalLineParser::TextDefinition *def = 
223                 CrystalLineParser::GetTextType(sExt.c_str());
224         if (def && def->encoding != -1)
225                 m_nSourceEncoding = def->encoding;
226         
227         UniFile *pufile = new UniMemFile;
228
229         // Now we only use the UniFile interface
230         // which is something we could implement for HTTP and/or FTP files
231
232         if (!pufile->OpenReadOnly(pszFileName))
233         {
234                 nRetVal = FileLoadResult::FRESULT_ERROR;
235                 UniFile::UniError uniErr = pufile->GetLastUniError();
236                 if (uniErr.HasError())
237                 {
238                         sError = uniErr.GetError();
239                 }
240                 InitNew(); // leave crystal editor in valid, empty state
241         }
242         else
243         {
244                 if (!m_unpackerSubcodes.empty())
245                 {
246                         // re-detect codepage
247                         int iGuessEncodingType = GetOptionsMgr()->GetInt(OPT_CP_DETECT);
248                         FileTextEncoding encoding2 = codepage_detect::Guess(pszFileName, iGuessEncodingType);
249                         pufile->SetUnicoding(encoding2.m_unicoding);
250                         pufile->SetCodepage(encoding2.m_codepage);
251                         pufile->SetBom(encoding2.m_bom);
252                         if (encoding2.m_bom)
253                                 pufile->ReadBom();
254                 }
255                 else
256                 {
257                         // If the file is not unicode file, use the codepage we were given to
258                         // interpret the 8-bit characters. If the file is unicode file,
259                         // determine its type (IsUnicode() does that).
260                         if (encoding.m_unicoding == ucr::NONE  || !pufile->IsUnicode())
261                                 pufile->SetCodepage(encoding.m_codepage);
262                 }
263                 unsigned lineno = 0;
264                 String eol, preveol;
265                 String sline;
266                 bool done = false;
267
268                 // Manually grow line array exponentially
269                 size_t arraysize = 500;
270                 m_aLines.resize(arraysize);
271                 
272                 // preveol must be initialized for empty files
273                 preveol = _T("\n");
274                 
275                 do {
276                         bool lossy = false;
277                         done = !pufile->ReadString(sline, eol, &lossy);
278
279                         // if last line had no eol, we can quit
280                         if (done && preveol.empty())
281                                 break;
282                         // but if last line had eol, we add an extra (empty) line to buffer
283
284                         // Grow line array
285                         if (lineno == arraysize)
286                         {
287                                 // For smaller sizes use exponential growth, but for larger
288                                 // sizes grow by constant ratio. Unlimited exponential growth
289                                 // easily runs out of memory.
290                                 if (arraysize < 100 * 1024)
291                                         arraysize *= 2;
292                                 else
293                                         arraysize += 100 * 1024;
294                                 m_aLines.resize(arraysize);
295                         }
296
297                         sline += eol; // TODO: opportunity for optimization, as CString append is terrible
298                         if (lossy)
299                         {
300                                 // TODO: Should record lossy status of line
301                         }
302                         AppendLine(lineno, sline.c_str(), static_cast<int>(sline.length()));
303                         ++lineno;
304                         preveol = eol;
305
306                 } while (!done);
307
308                 // fix array size (due to our manual exponential growth
309                 m_aLines.resize(lineno);
310         
311                 
312                 //Try to determine current CRLF mode (most frequent)
313                 if (nCrlfStyle == CRLFSTYLE::AUTOMATIC)
314                 {
315                         nCrlfStyle = GetTextFileStyle(pufile->GetTxtStats());
316                 }
317                 ASSERT (nCrlfStyle != CRLFSTYLE::AUTOMATIC);
318                 SetCRLFMode(nCrlfStyle);
319                 
320                 //  At least one empty line must present
321                 // (view does not work for empty buffers)
322                 ASSERT(m_aLines.size() > 0);
323                 
324                 m_bInit = true;
325                 m_bModified = false;
326                 m_bUndoGroup = m_bUndoBeginGroup = false;
327                 m_nSyncPosition = m_nUndoPosition = 0;
328                 ASSERT(m_aUndoBuf.size() == 0);
329                 m_ptLastChange.x = m_ptLastChange.y = -1;
330                 
331                 FinishLoading();
332                 // flags don't need initialization because 0 is the default value
333
334                 // Set the return value : OK + info if the file is impure
335                 // A pure file is a file where EOL are consistent (all DOS, or all UNIX, or all MAC)
336                 // An impure file is a file with several EOL types
337                 // WinMerge may display impure files, but the default option is to unify the EOL
338                 // We return this info to the caller, so it may display a confirmation box
339                 if (IsTextFileStylePure(pufile->GetTxtStats()))
340                         nRetVal = FileLoadResult::FRESULT_OK;
341                 else
342                         nRetVal = FileLoadResult::FRESULT_OK_IMPURE;
343
344                 // stash original encoding away
345                 m_encoding.m_unicoding = pufile->GetUnicoding();
346                 m_encoding.m_bom = pufile->HasBom();
347                 m_encoding.m_codepage = pufile->GetCodepage();
348
349                 if (pufile->GetTxtStats().nlosses)
350                 {
351                         FileLoadResult::AddModifier(nRetVal, FileLoadResult::FRESULT_LOSSY);
352                         readOnly = true;
353                 }
354         }
355         
356         // close the file now to free the handle
357         pufile->Close();
358         delete pufile;
359
360         // delete the file that unpacking may have created
361         if (tc::tcscmp(pszFileNameInit, pszFileName) != 0)
362         {
363                 try
364                 {
365                         TFile(pszFileName).remove();
366                 }
367                 catch (Exception& e)
368                 {
369                         LogErrorStringUTF8(e.displayText());
370                 }
371         }
372         return nRetVal;
373 }
374
375 /**
376  * @brief Saves file from buffer to disk
377  *
378  * @param bTempFile : false if we are saving user files and
379  * true if we are saving working-temp-files for diff-engine
380  *
381  * @return SAVE_DONE or an error code (list in MergeDoc.h)
382  */
383 int CDiffTextBuffer::SaveToFile (const String& pszFileName,
384                 bool bTempFile, String & sError, PackingInfo& infoUnpacker /*= nullptr*/,
385                 CRLFSTYLE nCrlfStyle /*= CRLFSTYLE::AUTOMATIC*/,
386                 bool bClearModifiedFlag /*= true*/,
387                 int nStartLine /*= 0*/, int nLines /*= -1*/)
388 {
389         ASSERT (nCrlfStyle == CRLFSTYLE::AUTOMATIC || nCrlfStyle == CRLFSTYLE::DOS ||
390                 nCrlfStyle == CRLFSTYLE::UNIX || nCrlfStyle == CRLFSTYLE::MAC);
391         ASSERT (m_bInit);
392
393         if (nLines == -1)
394                 nLines = static_cast<int>(m_aLines.size() - nStartLine);
395
396         if (pszFileName.empty())
397                 return SAVE_FAILED;     // No filename, cannot save...
398
399         if (nCrlfStyle == CRLFSTYLE::AUTOMATIC &&
400                 !GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL))
401         {
402                         // get the default nCrlfStyle of the CDiffTextBuffer
403                 nCrlfStyle = GetCRLFMode();
404                 ASSERT(nCrlfStyle != CRLFSTYLE::AUTOMATIC);
405         }
406
407         bool bOpenSuccess = true;
408         bool bSaveSuccess = false;
409
410         UniStdioFile file;
411
412         String sIntermediateFilename; // used when !bTempFile
413
414         if (bTempFile)
415         {
416                 file.SetUnicoding(ucr::UTF8);
417                 file.SetBom(GetOptionsMgr()->GetInt(OPT_CMP_DIFF_ALGORITHM) == 0);
418                 bOpenSuccess = !!file.OpenCreate(pszFileName);
419         }
420         else
421         {
422                 file.SetUnicoding(m_encoding.m_unicoding);
423                 file.SetBom(m_encoding.m_bom);
424                 file.SetCodepage(m_encoding.m_codepage);
425                 sIntermediateFilename = env::GetTemporaryFileName(m_strTempPath,
426                         _T("MRG_"), nullptr);
427                 if (sIntermediateFilename.empty())
428                         return SAVE_FAILED;  //Nothing to do if even tempfile name fails
429                 bOpenSuccess = !!file.OpenCreate(sIntermediateFilename);
430         }
431
432         if (!bOpenSuccess)
433         {       
434                 UniFile::UniError uniErr = file.GetLastUniError();
435                 if (uniErr.HasError())
436                 {
437                         sError = uniErr.GetError();
438                         if (bTempFile)
439                                 LogErrorString(strutils::format(_T("Opening file %s failed: %s"),
440                                         pszFileName, sError));
441                         else
442                                 LogErrorString(strutils::format(_T("Opening file %s failed: %s"),
443                                         sIntermediateFilename, sError));
444                 }
445                 return SAVE_FAILED;
446         }
447
448         const size_t StdioBufSize = (std::min)(512 * 1024, BUFSIZ + nLines * 32);
449         file.SetVBuf(_IOFBF, StdioBufSize);
450         file.WriteBom();
451
452         // line loop : get each real line and write it in the file
453         String sLine;
454         String sEol = GetStringEol(nCrlfStyle);
455         int lastRealLine = ApparentLastRealLine();
456         for (int line = nStartLine; line < nStartLine + nLines; ++line)
457         {
458                 if (GetLineFlags(line) & LF_GHOST)
459                         continue;
460
461                 // get the characters of the line (excluding EOL)
462                 if (GetLineLength(line) > 0)
463                 {
464                         int nLineLength = GetLineLength(line);
465                         sLine.resize(0);
466                         sLine.reserve(nLineLength + 4);
467                         sLine.append(GetLineChars(line), nLineLength);
468                 }
469                 else
470                         sLine.clear();
471
472                 if (bTempFile && m_bTableEditing && m_bAllowNewlinesInQuotes)
473                 {
474                         strutils::replace(sLine, _T("\x1b"), _T("\x1b\x1b"));
475                         strutils::replace(sLine, _T("\r"), _T("\x1br"));
476                         strutils::replace(sLine, _T("\n"), _T("\x1bn"));
477                 }
478
479                 // last real line ?
480                 if (line == lastRealLine || lastRealLine == -1 )
481                 {
482                         // If original last line had no EOL, then we are done
483                         if( !m_aLines[line].HasEol() )
484                         {
485                                 file.WriteString(sLine);
486                                 break;
487                         }
488                         // Otherwise, add the appropriate EOL to the last line ...
489                 }
490
491                 // normal line : append an EOL
492                 if (nCrlfStyle == CRLFSTYLE::AUTOMATIC || nCrlfStyle == CRLFSTYLE::MIXED)
493                 {
494                         // either the EOL of the line (when preserve original EOL chars is on)
495                         sLine += GetLineEol(line);
496                 }
497                 else
498                 {
499                         // or the default EOL for this file
500                         sLine += sEol;
501                 }
502
503                 // write this line to the file (codeset or unicode conversions are done there
504                 file.WriteString(sLine);
505
506                 if (line == lastRealLine || lastRealLine == -1)
507                 {
508                         // Last line, so now done
509                         break;
510                 }
511         }
512         file.Close();
513
514         if (!bTempFile)
515         {
516                 // If we are saving user files
517                 // we need an unpacker/packer, at least a "do nothing" one
518                 // repack the file here, overwrite the temporary file we did save in
519                 bSaveSuccess = infoUnpacker.Packing(sIntermediateFilename, pszFileName, m_unpackerSubcodes, { pszFileName });
520                 if (!bSaveSuccess)
521                         sError = GetSysError();
522                 try
523                 {
524                         TFile(sIntermediateFilename).remove();
525                 }
526                 catch (Exception& e)
527                 {
528                         LogErrorStringUTF8(e.displayText());
529                 }
530                 if (!bSaveSuccess)
531                 {
532                         // returns now, don't overwrite the original file
533                         return m_unpackerSubcodes.empty() ? SAVE_FAILED : SAVE_PACK_FAILED;
534                 }
535
536                 if (bClearModifiedFlag)
537                 {
538                         SetModified(false);
539                         m_nSyncPosition = m_nUndoPosition;
540                 }
541
542                 // remember revision number on save
543                 m_dwRevisionNumberOnSave = m_dwCurrentRevisionNumber;
544
545                 // redraw line revision marks
546                 UpdateViews (nullptr, nullptr, UPDATE_FLAGSONLY);       
547         }
548         else
549         {
550                 if (bClearModifiedFlag)
551                 {
552                         SetModified(false);
553                         m_nSyncPosition = m_nUndoPosition;
554                 }
555                 bSaveSuccess = true;
556         }
557
558         if (bSaveSuccess)
559                 return SAVE_DONE;
560         else
561                 return SAVE_FAILED;
562 }
563
564 /// Replace line (removing any eol, and only including one if in strText)
565 void CDiffTextBuffer::ReplaceFullLines(CDiffTextBuffer& dbuf, CDiffTextBuffer& sbuf, CCrystalTextView * pSource, int nLineBegin, int nLineEnd, int nAction /*=CE_ACTION_UNKNOWN*/)
566 {
567         String strText;
568         if (nLineBegin != nLineEnd || sbuf.GetLineLength(nLineEnd) > 0)
569                 sbuf.GetTextWithoutEmptys(nLineBegin, 0, nLineEnd, sbuf.GetLineLength(nLineEnd), strText);
570         strText += sbuf.GetLineEol(nLineEnd);
571
572         if (nLineBegin != nLineEnd || dbuf.GetFullLineLength(nLineEnd) > 0)
573         {
574                 int nLineEndSource = nLineEnd < dbuf.GetLineCount() ? nLineEnd : dbuf.GetLineCount();
575                 if (nLineEnd+1 < GetLineCount())
576                         dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource + 1, 0, nAction);
577                 else
578                         dbuf.DeleteText(pSource, nLineBegin, 0, nLineEndSource, dbuf.GetLineLength(nLineEndSource), nAction); 
579         }
580
581         if (size_t cchText = strText.length())
582         {
583                 int endl,endc;
584                 dbuf.InsertText(pSource, nLineBegin, 0, strText.c_str(), cchText, endl, endc, nAction);
585         }
586 }
587
588 bool CDiffTextBuffer::curUndoGroup()
589 {
590         return (m_aUndoBuf.size() != 0 && m_aUndoBuf[0].m_dwFlags&UNDO_BEGINGROUP);
591 }
592
593 bool CDiffTextBuffer::                  /* virtual override */
594 DeleteText2(CCrystalTextView * pSource, int nStartLine, int nStartChar,
595         int nEndLine, int nEndChar, int nAction /*= CE_ACTION_UNKNOWN*/, bool bHistory /*= true*/)
596 {
597         for (auto syncpnt : m_pOwnerDoc->GetSyncPointList())
598         {
599                 const int nLineSyncPoint = syncpnt[m_nThisPane];
600                 if (((nStartChar == 0 && nStartLine == nLineSyncPoint) || nStartLine < nLineSyncPoint) &&
601                         nLineSyncPoint < nEndLine)
602                         m_pOwnerDoc->DeleteSyncPoint(m_nThisPane, nLineSyncPoint, false);
603         }
604         return CGhostTextBuffer::DeleteText2(pSource, nStartLine, nStartChar, nEndLine, nEndChar, nAction, bHistory);
605 }