OSDN Git Service

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