OSDN Git Service

Use std::vector instead of CArray for storing linedata in editor.
[winmerge-jp/winmerge-jp.git] / Src / DiffTextBuffer.cpp
1 /** 
2  * @file  DiffTextBuffer.cpp
3  *
4  * @brief Implementation file for CDiffTextBuffer
5  *
6  */
7 // ID line follows -- this is updated by SVN
8 // $Id$
9
10 #include "StdAfx.h"
11 #include "UniFile.h"
12 #include "files.h"
13 #include "cs2cs.h"
14 #include "locality.h"
15 #include "coretools.h"
16 #include "Merge.h"
17 #include "OptionsDef.h"
18 #include "Environment.h"
19 #include "MergeLineFlags.h"
20 #include "MergeDoc.h"
21 #include "FileTransform.h"
22 #include "FileTextEncoding.h"
23 #include "DiffTextBuffer.h"
24
25 #ifdef _DEBUG
26 #define new DEBUG_NEW
27 #undef THIS_FILE
28 static char THIS_FILE[] = __FILE__;
29 #endif
30
31 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats);
32 static CString GetLineByteTimeReport(UINT lines, __int64 bytes,
33         const COleDateTime & start);
34 static void EscapeControlChars(CString &s);
35 static LPCTSTR GetEol(const CString &str);
36 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats);
37
38 /**
39  * @brief Check if file has only one EOL type.
40  * @param [in] stats File's text stats.
41  * @return true if only one EOL type is found, false otherwise.
42  */
43 static bool IsTextFileStylePure(const UniMemFile::txtstats & stats)
44 {
45         int nType = 0;
46         nType += (stats.ncrlfs > 0);
47         nType += (stats.ncrs > 0);
48         nType += (stats.nlfs > 0);
49         return (nType <= 1);
50 }
51
52 /**
53  * @brief Return a string giving #lines and #bytes and how much time elapsed.
54  * @param [in] lines Count of lines.
55  * @param [in] bytes Count of bytes.
56  * @param [in] start Time used.
57  * @return Formatted string.
58  */
59 static CString GetLineByteTimeReport(UINT lines, __int64 bytes,
60         const COleDateTime & start)
61 {
62         String sLines = locality::NumToLocaleStr((int)lines);
63         String sBytes = locality::NumToLocaleStr(bytes);
64         COleDateTimeSpan duration = COleDateTime::GetCurrentTime() - start;
65         String sMinutes = locality::NumToLocaleStr((int)duration.GetTotalMinutes());
66         CString str;
67         str.Format(_T("%s lines (%s byte) saved in %sm%02ds")
68                 , sLines.c_str(), sBytes.c_str(), sMinutes.c_str()
69                 , duration.GetSeconds()
70                 );
71         return str;
72 }
73
74 /**
75  * @brief Escape control characters.
76  * @param [in,out] s Line of text excluding eol chars.
77  *
78  * @note Escape sequences follow the pattern
79  * (leadin character, high nibble, low nibble, leadout character).
80  * The leadin character is '\x0F'. The leadout character is a backslash.
81  */
82 static void EscapeControlChars(CString &s)
83 {
84         // Compute buffer length required for escaping
85         int n = s.GetLength();
86         LPCTSTR q = s;
87         int i = n;
88         while (i)
89         {
90                 TCHAR c = q[--i];
91                 // Is it a control character in the range 0..31 except TAB?
92                 if (!(c & ~_T('\x1F')) && c != _T('\t'))
93                 {
94                         n += 3; // Need 3 extra characters to escape
95                 }
96         }
97         // Reallocate accordingly
98         i = s.GetLength();
99         LPTSTR p = s.GetBufferSetLength(n);
100         // Copy/translate characters starting at end of string
101         while (i)
102         {
103                 TCHAR c = p[--i];
104                 // Is it a control character in the range 0..31 except TAB?
105                 if (!(c & ~_T('\x1F')) && c != _T('\t'))
106                 {
107                         // Bitwise OR with 0x100 so _itot() will output 3 hex digits
108                         _itot(0x100 | c, p + n - 4, 16);
109                         // Replace terminating zero with leadout character
110                         p[n - 1] = _T('\\');
111                         // Prepare to replace 1st hex digit with leadin character
112                         c = _T('\x0F');
113                         n -= 3;
114                 }
115                 p[--n] = c;
116         }
117 }
118
119 /**
120  * @brief Get EOL of the string.
121  * This function returns a pointer to the EOL chars in the given string.
122  * Behavior is similar to CCrystalTextBuffer::GetLineEol().
123  * @param [in] str String whose EOL chars are returned.
124  * @return Pointer to string's EOL chars, or empty string if no EOL found.
125  */
126 static LPCTSTR GetEol(const CString &str)
127 {
128         if (str.GetLength()>1 && str[str.GetLength()-2]=='\r' && str[str.GetLength()-1]=='\n')
129                 return (LPCTSTR)str + str.GetLength()-2;
130         if (str.GetLength()>0 && (str[str.GetLength()-1]=='\r' || str[str.GetLength()-1]=='\n'))
131                 return (LPCTSTR)str + str.GetLength()-1;
132         return _T("");
133 }
134
135 /**
136  * @brief Get file's EOL type.
137  * @param [in] stats File's text stats.
138  * @return EOL type.
139  */
140 static CRLFSTYLE GetTextFileStyle(const UniMemFile::txtstats & stats)
141 {
142         if (stats.ncrlfs >= stats.nlfs)
143         {
144                 if (stats.ncrlfs >= stats.ncrs)
145                         return CRLF_STYLE_DOS;
146                 else
147                         return CRLF_STYLE_MAC;
148         }
149         else
150         {
151                 if (stats.nlfs >= stats.ncrs)
152                         return CRLF_STYLE_UNIX;
153                 else
154                         return CRLF_STYLE_MAC;
155         }
156 }
157
158 /**
159  * @brief Constructor.
160  * @param [in] pDoc Owning CMergeDoc.
161  * @param [in] pane Pane number this buffer is associated with.
162  */
163 CDiffTextBuffer::CDiffTextBuffer(CMergeDoc * pDoc, int pane)
164 : m_pOwnerDoc(pDoc)
165 , m_nThisPane(pane)
166 , m_unpackerSubcode(0)
167 , m_bMixedEOL(false)
168 {
169 }
170
171 /**
172  * @brief Get a line from the buffer.
173  * @param [in] nLineIndex Index of the line to get.
174  * @param [out] strLine Returns line text in the index.
175  */
176 BOOL CDiffTextBuffer::GetLine(int nLineIndex, CString &strLine)
177 {
178         int nLineLength = CCrystalTextBuffer::GetLineLength(nLineIndex);
179         if (nLineLength < 0)
180                 return FALSE;
181         else if (nLineLength == 0)
182                 strLine.Empty();
183         else
184         {
185                 _tcsncpy(strLine.GetBuffer(nLineLength + 1),
186                         CCrystalTextBuffer::GetLineChars(nLineIndex), nLineLength);
187                 strLine.ReleaseBuffer(nLineLength);
188         }
189         return TRUE;
190 }
191
192 /**
193  * @brief Set the buffer modified status.
194  * @param [in] bModified New modified status, TRUE if buffer has been
195  *   modified since last saving.
196  */
197 void CDiffTextBuffer::SetModified(BOOL bModified /*= TRUE*/)
198 {
199         CCrystalTextBuffer::SetModified (bModified);
200         m_pOwnerDoc->SetModifiedFlag (bModified);
201 }
202
203 /**
204  * @brief Get a line (with EOL bytes) from the buffer.
205  * This function is like GetLine() but it also includes line's EOL to the
206  * returned string.
207  * @param [in] nLineIndex Index of the line to get.
208  * @param [out] strLine Returns line text in the index. Existing content
209  * of this string is overwritten.
210  */
211 BOOL CDiffTextBuffer::GetFullLine(int nLineIndex, CString &strLine)
212 {
213         int cchText = GetFullLineLength(nLineIndex);
214         if (cchText == 0)
215         {
216                 strLine.Empty();
217                 return FALSE;
218         }
219         LPTSTR pchText = strLine.GetBufferSetLength(cchText);
220         memcpy(pchText, GetLineChars(nLineIndex), cchText * sizeof(TCHAR));
221         return TRUE;
222 }
223
224 void CDiffTextBuffer::AddUndoRecord(BOOL bInsert, const CPoint & ptStartPos,
225                 const CPoint & ptEndPos, LPCTSTR pszText, int cchText,
226                 int nLinesToValidate, int nActionType /*= CE_ACTION_UNKNOWN*/,
227                 CDWordArray *paSavedRevisonNumbers)
228 {
229         CGhostTextBuffer::AddUndoRecord(bInsert, ptStartPos, ptEndPos, pszText,
230                 cchText, nLinesToValidate, nActionType, paSavedRevisonNumbers);
231         if (m_aUndoBuf[m_nUndoPosition - 1].m_dwFlags & UNDO_BEGINGROUP)
232         {
233                 m_pOwnerDoc->undoTgt.erase(m_pOwnerDoc->curUndo, m_pOwnerDoc->undoTgt.end());
234                 m_pOwnerDoc->undoTgt.push_back(m_pOwnerDoc->GetView(m_nThisPane));
235                 m_pOwnerDoc->curUndo = m_pOwnerDoc->undoTgt.end();
236         }
237 }
238 /**
239  * @brief Checks if a flag is set for line.
240  * @param [in] line Index (0-based) for line.
241  * @param [in] flag Flag to check.
242  * @return TRUE if flag is set, FALSE otherwise.
243  */
244 BOOL CDiffTextBuffer::FlagIsSet(UINT line, DWORD flag)
245 {
246         return ((m_aLines[line].m_dwFlags & flag) == flag);
247 }
248
249 /**
250 Remove blank lines and clear winmerge flags
251 (2003-06-21, Perry: I don't understand why this is necessary, but if this isn't 
252 done, more and more gray lines appear in the file)
253 (2003-07-31, Laoran I don't understand either why it is necessary, but it works
254 fine, so let's go on with it)
255 */
256 void CDiffTextBuffer::prepareForRescan()
257 {
258         RemoveAllGhostLines();
259         for (int ct = GetLineCount() - 1; ct >= 0; --ct)
260         {
261                 SetLineFlag(ct, LF_DIFF, FALSE, FALSE, FALSE);
262                 SetLineFlag(ct, LF_TRIVIAL, FALSE, FALSE, FALSE);
263                 SetLineFlag(ct, LF_MOVED, FALSE, FALSE, FALSE);
264         }
265 }
266
267 /** 
268  * @brief Called when line has been edited.
269  * After editing a line, we don't know if there is a diff or not.
270  * So we clear the LF_DIFF flag (and it is more easy to read during edition).
271  * Rescan will set the proper color.
272  * @param [in] nLine Line that has been edited.
273  */
274
275 void CDiffTextBuffer::OnNotifyLineHasBeenEdited(int nLine)
276 {
277         SetLineFlag(nLine, LF_DIFF, FALSE, FALSE, FALSE);
278         SetLineFlag(nLine, LF_TRIVIAL, FALSE, FALSE, FALSE);
279         SetLineFlag(nLine, LF_MOVED, FALSE, FALSE, FALSE);
280         CGhostTextBuffer::OnNotifyLineHasBeenEdited(nLine);
281 }
282
283 /**
284  * @brief Set the folder for temp files.
285  * @param [in] path Temp files folder.
286  */
287 void CDiffTextBuffer::SetTempPath(const String &path)
288 {
289         m_strTempPath = path;
290 }
291
292 /**
293  * @brief Is the buffer initialized?
294  * @return TRUE if the buffer is initialized, FALSE otherwise.
295  */
296 bool CDiffTextBuffer::IsInitialized() const
297 {
298         return !!m_bInit;
299 }
300
301 /**
302  * @brief Load file from disk into buffer
303  *
304  * @param [in] pszFileNameInit File to load
305  * @param [in] infoUnpacker Unpacker plugin
306  * @param [in] sToFindUnpacker String for finding unpacker plugin
307  * @param [out] readOnly Loading was lossy so file should be read-only
308  * @param [in] nCrlfStyle EOL style used
309  * @param [in] encoding Encoding used
310  * @param [out] sError Error message returned
311  * @return FRESULT_OK when loading succeed or (list in files.h):
312  * - FRESULT_OK_IMPURE : load OK, but the EOL are of different types
313  * - FRESULT_ERROR_UNPACK : plugin failed to unpack
314  * - FRESULT_ERROR : loading failed, sError contains error message
315  * - FRESULT_BINARY : file is binary file
316  * @note If this method fails, it calls InitNew so the CDiffTextBuffer is in a valid state
317  */
318 int CDiffTextBuffer::LoadFromFile(LPCTSTR pszFileNameInit,
319                 PackingInfo * infoUnpacker, LPCTSTR sToFindUnpacker, BOOL & readOnly,
320                 CRLFSTYLE nCrlfStyle, const FileTextEncoding & encoding, CString &sError)
321 {
322         ASSERT(!m_bInit);
323         ASSERT(m_aLines.size() == 0);
324
325         // Unpacking the file here, save the result in a temporary file
326         String sFileName(pszFileNameInit);
327         if (infoUnpacker->bToBeScanned)
328         {
329                 if (!FileTransform_Unpacking(sFileName, sToFindUnpacker, infoUnpacker,
330                         &m_unpackerSubcode))
331                 {
332                         InitNew(); // leave crystal editor in valid, empty state
333                         return FileLoadResult::FRESULT_ERROR_UNPACK;
334                 }
335         }
336         else
337         {
338                 if (!FileTransform_Unpacking(sFileName, infoUnpacker, &m_unpackerSubcode))
339                 {
340                         InitNew(); // leave crystal editor in valid, empty state
341                         return FileLoadResult::FRESULT_ERROR_UNPACK;
342                 }
343         }
344         // we use the same unpacker for both files, so it must be defined after first file
345         ASSERT(infoUnpacker->bToBeScanned != PLUGIN_AUTO);
346         // we will load the transformed file
347         LPCTSTR pszFileName = sFileName.c_str();
348
349         String sExt;
350         DWORD nRetVal = FileLoadResult::FRESULT_OK;
351
352         // Set encoding based on extension, if we know one
353         SplitFilename(pszFileName, NULL, NULL, &sExt);
354         CCrystalTextView::TextDefinition *def = 
355                 CCrystalTextView::GetTextType(sExt.c_str());
356         if (def && def->encoding != -1)
357                 m_nSourceEncoding = def->encoding;
358         
359         UniFile *pufile = infoUnpacker->pufile;
360         if (pufile == 0)
361                 pufile = new UniMemFile;
362
363         // Now we only use the UniFile interface
364         // which is something we could implement for HTTP and/or FTP files
365
366         if (!pufile->OpenReadOnly(pszFileName))
367         {
368                 nRetVal = FileLoadResult::FRESULT_ERROR;
369                 UniFile::UniError uniErr = pufile->GetLastUniError();
370                 if (uniErr.HasError())
371                 {
372                         sError = uniErr.GetError().c_str();
373                 }
374                 InitNew(); // leave crystal editor in valid, empty state
375                 goto LoadFromFileExit;
376         }
377         else
378         {
379                 // If the file is not unicode file, use the codepage we were given to
380                 // interpret the 8-bit characters. If the file is unicode file,
381                 // determine its type (IsUnicode() does that).
382                 if (encoding.m_unicoding == ucr::NONE || !pufile->IsUnicode())
383                         pufile->SetCodepage(encoding.m_codepage);
384                 UINT lineno = 0;
385                 String eol, preveol;
386                 String sline;
387                 bool done = false;
388                 UINT next_line_report = 100; // for trace messages
389                 UINT next_line_multiple = 5; // for trace messages
390                 COleDateTime start = COleDateTime::GetCurrentTime(); // for trace messages
391
392                 // Manually grow line array exponentially
393                 UINT arraysize = 500;
394                 m_aLines.resize(arraysize);
395                 
396                 // preveol must be initialized for empty files
397                 preveol = _T("\n");
398                 
399                 do {
400                         bool lossy = false;
401                         done = !pufile->ReadString(sline, eol, &lossy);
402
403                         // if last line had no eol, we can quit
404                         if (done && preveol.empty())
405                                 break;
406                         // but if last line had eol, we add an extra (empty) line to buffer
407
408                         // Grow line array
409                         if (lineno == arraysize)
410                         {
411                                 // For smaller sizes use exponential growth, but for larger
412                                 // sizes grow by constant ratio. Unlimited exponential growth
413                                 // easily runs out of memory.
414                                 if (arraysize < 100 * 1024)
415                                         arraysize *= 2;
416                                 else
417                                         arraysize += 100 * 1024;
418                                 m_aLines.resize(arraysize);
419                         }
420
421                         sline += eol; // TODO: opportunity for optimization, as CString append is terrible
422                         if (lossy)
423                         {
424                                 // TODO: Should record lossy status of line
425                         }
426                         AppendLine(lineno, sline.c_str(), sline.length());
427                         ++lineno;
428                         preveol = eol;
429
430 #ifdef _DEBUG
431                         // send occasional line counts to trace
432                         // (at 100, 500, 1000, 5000, etc)
433                         if (lineno == next_line_report)
434                         {
435                                 __int64 dwBytesRead = pufile->GetPosition();
436                                 COleDateTimeSpan duration = COleDateTime::GetCurrentTime() - start;
437                                 if (duration.GetTotalMinutes() > 0)
438                                 {
439                                         CString strace = GetLineByteTimeReport(lineno, dwBytesRead, start);
440                                         TRACE(_T("%s\n"), (LPCTSTR)strace);
441                                 }
442                                 next_line_report = next_line_multiple * next_line_report;
443                                 next_line_multiple = (next_line_multiple == 5) ? 2 : 5;
444                         }
445 #endif // _DEBUG
446                 } while (!done);
447
448 #ifdef _DEBUG
449                 // Send report of duration to trace (if it took a while)
450                 COleDateTime end = COleDateTime::GetCurrentTime();
451                 COleDateTimeSpan duration = end - start;
452                 if (duration.GetTotalMinutes() > 0)
453                 {
454                         __int64 dwBytesRead = pufile->GetPosition();
455                         CString strace = GetLineByteTimeReport(lineno, dwBytesRead, start);
456                         TRACE(_T("%s\n"), (LPCTSTR)strace);
457                 }
458 #endif // _DEBUG
459
460                 // fix array size (due to our manual exponential growth
461                 m_aLines.resize(lineno);
462         
463                 
464                 //Try to determine current CRLF mode (most frequent)
465                 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC)
466                 {
467                         nCrlfStyle = GetTextFileStyle(pufile->GetTxtStats());
468                 }
469                 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 2);
470                 SetCRLFMode(nCrlfStyle);
471                 
472                 //  At least one empty line must present
473                 // (view does not work for empty buffers)
474                 ASSERT(m_aLines.size() > 0);
475                 
476                 m_bInit = TRUE;
477                 m_bModified = FALSE;
478                 m_bUndoGroup = m_bUndoBeginGroup = FALSE;
479                 m_nSyncPosition = m_nUndoPosition = 0;
480                 ASSERT(m_aUndoBuf.size() == 0);
481                 m_ptLastChange.x = m_ptLastChange.y = -1;
482                 
483                 FinishLoading();
484                 // flags don't need initialization because 0 is the default value
485
486                 // Set the return value : OK + info if the file is impure
487                 // A pure file is a file where EOL are consistent (all DOS, or all UNIX, or all MAC)
488                 // An impure file is a file with several EOL types
489                 // WinMerge may display impure files, but the default option is to unify the EOL
490                 // We return this info to the caller, so it may display a confirmation box
491                 if (IsTextFileStylePure(pufile->GetTxtStats()))
492                         nRetVal = FileLoadResult::FRESULT_OK;
493                 else
494                         nRetVal = FileLoadResult::FRESULT_OK_IMPURE;
495
496                 // stash original encoding away
497                 m_encoding.m_unicoding = pufile->GetUnicoding();
498                 m_encoding.m_bom = pufile->HasBom();
499                 m_encoding.m_codepage = pufile->GetCodepage();
500
501                 if (pufile->GetTxtStats().nlosses)
502                 {
503                         FileLoadResult::AddModifier(nRetVal, FileLoadResult::FRESULT_LOSSY);
504                         readOnly = TRUE;
505                 }
506         }
507         
508 LoadFromFileExit:
509         // close the file now to free the handle
510         pufile->Close();
511         delete pufile;
512
513         // delete the file that unpacking may have created
514         if (_tcscmp(pszFileNameInit, pszFileName) != 0)
515                 if (!::DeleteFile(pszFileName))
516                 {
517                         LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
518                                 pszFileName, GetSysError(GetLastError())));
519                 }
520
521         return nRetVal;
522 }
523
524 /**
525  * @brief Saves file from buffer to disk
526  *
527  * @param bTempFile : FALSE if we are saving user files and
528  * TRUE if we are saving workin-temp-files for diff-engine
529  *
530  * @return SAVE_DONE or an error code (list in MergeDoc.h)
531  */
532 int CDiffTextBuffer::SaveToFile (LPCTSTR pszFileName,
533                 BOOL bTempFile, String & sError, PackingInfo * infoUnpacker /*= NULL*/,
534                 CRLFSTYLE nCrlfStyle /*= CRLF_STYLE_AUTOMATIC*/,
535                 BOOL bClearModifiedFlag /*= TRUE*/ )
536 {
537         ASSERT (m_bInit);
538
539         if (!pszFileName || _tcslen(pszFileName) == 0)
540                 return SAVE_FAILED;     // No filename, cannot save...
541
542         if (nCrlfStyle == CRLF_STYLE_AUTOMATIC &&
543                 !GetOptionsMgr()->GetBool(OPT_ALLOW_MIXED_EOL) ||
544                 infoUnpacker && infoUnpacker->disallowMixedEOL)
545         {
546                         // get the default nCrlfStyle of the CDiffTextBuffer
547                 nCrlfStyle = GetCRLFMode();
548                 ASSERT(nCrlfStyle >= 0 && nCrlfStyle <= 2);
549         }
550
551         BOOL bOpenSuccess = TRUE;
552         BOOL bSaveSuccess = FALSE;
553
554         UniStdioFile file;
555         file.SetUnicoding(m_encoding.m_unicoding);
556         file.SetBom(m_encoding.m_bom);
557         file.SetCodepage(m_encoding.m_codepage);
558
559         String sIntermediateFilename; // used when !bTempFile
560
561         if (bTempFile)
562         {
563                 bOpenSuccess = !!file.OpenCreate(pszFileName);
564         }
565         else
566         {
567                 sIntermediateFilename = env_GetTempFileName(m_strTempPath.c_str(),
568                         _T("MRG_"), NULL);
569                 if (sIntermediateFilename.empty())
570                         return SAVE_FAILED;  //Nothing to do if even tempfile name fails
571                 bOpenSuccess = !!file.OpenCreate(sIntermediateFilename.c_str());
572         }
573
574         if (!bOpenSuccess)
575         {       
576                 UniFile::UniError uniErr = file.GetLastUniError();
577                 if (uniErr.HasError())
578                 {
579                         sError = uniErr.GetError().c_str();
580                         if (bTempFile)
581                                 LogErrorString(Fmt(_T("Opening file %s failed: %s"),
582                                         pszFileName, sError));
583                         else
584                                 LogErrorString(Fmt(_T("Opening file %s failed: %s"),
585                                         sIntermediateFilename, sError));
586                 }
587                 return SAVE_FAILED;
588         }
589
590         file.WriteBom();
591
592         // line loop : get each real line and write it in the file
593         CString sLine;
594         CString sEol = GetStringEol(nCrlfStyle);
595         int nLineCount = m_aLines.size();
596         for (int line = 0; line < nLineCount; ++line)
597         {
598                 if (GetLineFlags(line) & LF_GHOST)
599                         continue;
600
601                 // get the characters of the line (excluding EOL)
602                 if (GetLineLength(line) > 0)
603                         GetText(line, 0, line, GetLineLength(line), sLine, 0);
604                 else
605                         sLine = _T("");
606
607                 if (bTempFile)
608                         EscapeControlChars(sLine);
609                 // last real line ?
610                 if (line == ApparentLastRealLine())
611                 {
612                         // last real line is never EOL terminated
613                         ASSERT (_tcslen(GetLineEol(line)) == 0);
614                         // write the line and exit loop
615                         String tmpLine(sLine);
616                         file.WriteString(tmpLine);
617                         break;
618                 }
619
620                 // normal real line : append an EOL
621                 if (nCrlfStyle == CRLF_STYLE_AUTOMATIC)
622                 {
623                         // either the EOL of the line (when preserve original EOL chars is on)
624                         sLine += GetLineEol(line);
625                 }
626                 else
627                 {
628                         // or the default EOL for this file
629                         sLine += sEol;
630                 }
631
632                 // write this line to the file (codeset or unicode conversions are done there)
633                 String tmpLine(sLine);
634                 file.WriteString(tmpLine);
635         }
636         file.Close();
637
638         if (!bTempFile)
639         {
640                 // If we are saving user files
641                 // we need an unpacker/packer, at least a "do nothing" one
642                 ASSERT(infoUnpacker != NULL);
643                 // repack the file here, overwrite the temporary file we did save in
644                 String csTempFileName = sIntermediateFilename;
645                 infoUnpacker->subcode = m_unpackerSubcode;
646                 if (!FileTransform_Packing(csTempFileName, *infoUnpacker))
647                 {
648                         if (!::DeleteFile(sIntermediateFilename.c_str()))
649                         {
650                                 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
651                                         sIntermediateFilename.c_str(), GetSysError(GetLastError())));
652                         }
653                         // returns now, don't overwrite the original file
654                         return SAVE_PACK_FAILED;
655                 }
656                 // the temp filename may have changed during packing
657                 if (csTempFileName != sIntermediateFilename)
658                 {
659                         if (!::DeleteFile(sIntermediateFilename.c_str()))
660                         {
661                                 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
662                                         sIntermediateFilename.c_str(), GetSysError(GetLastError())));
663                         }
664                         sIntermediateFilename = csTempFileName;
665                 }
666
667                 // Write tempfile over original file
668                 if (::CopyFile(sIntermediateFilename.c_str(), pszFileName, FALSE))
669                 {
670                         if (!::DeleteFile(sIntermediateFilename.c_str()))
671                         {
672                                 LogErrorString(Fmt(_T("DeleteFile(%s) failed: %s"),
673                                         sIntermediateFilename.c_str(), GetSysError(GetLastError())));
674                         }
675                         if (bClearModifiedFlag)
676                         {
677                                 SetModified(FALSE);
678                                 m_nSyncPosition = m_nUndoPosition;
679                         }
680                         bSaveSuccess = TRUE;
681
682                         // remember revision number on save
683                         m_dwRevisionNumberOnSave = m_dwCurrentRevisionNumber;
684
685                         // redraw line revision marks
686                         UpdateViews (NULL, NULL, UPDATE_FLAGSONLY);     
687                 }
688                 else
689                 {
690                         sError = GetSysError(GetLastError());
691                         LogErrorString(Fmt(_T("CopyFile(%s, %s) failed: %s"),
692                                 sIntermediateFilename.c_str(), pszFileName, sError));
693                 }
694         }
695         else
696         {
697                 if (bClearModifiedFlag)
698                 {
699                         SetModified(FALSE);
700                         m_nSyncPosition = m_nUndoPosition;
701                 }
702                 bSaveSuccess = TRUE;
703         }
704
705         if (bSaveSuccess)
706                 return SAVE_DONE;
707         else
708                 return SAVE_FAILED;
709 }
710
711 /**
712  * @brief Replace a line with new text.
713  * This function replaces line's text without changing the EOL style/bytes
714  * of the line.
715  * @param [in] pSource Editor view where text is changed.
716  * @param [in] nLine Index of the line to change.
717  * @param [in] pchText New text of the line.
718  * @param [in] cchText New length of the line (not inc. EOL bytes).
719  * @param [in] nAction Edit action to use.
720  */
721 void CDiffTextBuffer::ReplaceLine(CCrystalTextView * pSource, int nLine,
722                 LPCTSTR pchText, int cchText, int nAction /*=CE_ACTION_UNKNOWN*/)
723 {
724         if (GetLineLength(nLine)>0)
725                 DeleteText(pSource, nLine, 0, nLine, GetLineLength(nLine), nAction);
726         int endl, endc;
727         if (cchText)
728                 InsertText(pSource, nLine, 0, pchText, cchText, endl, endc, nAction);
729 }
730
731 /// Replace line (removing any eol, and only including one if in strText)
732 /**
733  * @brief Replace a line with new text.
734  * This function replaces line's text including EOL bytes. If the @p strText
735  * does not include EOL bytes, the "line" does not get EOL bytes.
736  * @param [in] pSource Editor view where text is changed.
737  * @param [in] nLine Index of the line to change.
738  * @param [in] pchText New text of the line.
739  * @param [in] cchText New length of the line (not inc. EOL bytes).
740  * @param [in] nAction Edit action to use.
741  */
742 void CDiffTextBuffer::ReplaceFullLine(CCrystalTextView * pSource, int nLine,
743                 const CString &strText, int nAction /*=CE_ACTION_UNKNOWN*/)
744 {
745         LPCTSTR eol = GetEol(strText);
746         if (_tcscmp(GetLineEol(nLine), eol) == 0)
747         {
748                 // (optimization) eols are the same, so just replace text inside line
749                 // we must clean strText from its eol...
750                 int eolLength = _tcslen(eol);
751                 ReplaceLine(pSource, nLine, strText, strText.GetLength() - eolLength, nAction);
752                 return;
753         }
754
755         // we may need a last line as the DeleteText end is (x=0,y=line+1)
756         if (nLine + 1 == GetLineCount())
757                 InsertGhostLine (pSource, GetLineCount());
758
759         if (GetFullLineLength(nLine))
760                 DeleteText(pSource, nLine, 0, nLine + 1, 0, nAction); 
761         int endl, endc;
762         const int cchText = strText.GetLength();
763         if (cchText)
764                 InsertText(pSource, nLine, 0, strText, cchText, endl, endc, nAction);
765 }
766
767 bool CDiffTextBuffer::curUndoGroup()
768 {
769         return (m_aUndoBuf.size() != 0 && m_aUndoBuf[0].m_dwFlags&UNDO_BEGINGROUP);
770 }