OSDN Git Service

Improve plugin system (#797) (6)
[winmerge-jp/winmerge-jp.git] / Src / GhostTextBuffer.cpp
1 ////////////////////////////////////////////////////////////////////////////
2 //  File:       GhostTextBuffer.cpp
3 //  Version:    1.0.0.0
4 //  Created:    31-Jul-2003
5 //
6 /////////////////////////////////////////////////////////////////////////////
7 //    WinMerge:  an interactive diff/merge utility
8 //    Copyright (C) 1997-2000  Thingamahoochie Software
9 //    Author: Dean Grimm
10 //    SPDX-License-Identifier: GPL-2.0-or-later
11 /////////////////////////////////////////////////////////////////////////////
12 /** 
13  * @file  GhostTextBuffer.cpp
14  *
15  * @brief Implementation of GhostTextBuffer class.
16  */
17
18 #include "StdAfx.h"
19 #include "GhostTextBuffer.h"
20 #include "MergeLineFlags.h"
21
22 #ifdef _DEBUG
23 #define new DEBUG_NEW
24 #endif
25
26 using std::vector;
27
28 BEGIN_MESSAGE_MAP (CGhostTextBuffer, CCrystalTextBuffer)
29 //{{AFX_MSG_MAP(CGhostTextBuffer)
30 //}}AFX_MSG_MAP
31 END_MESSAGE_MAP ()
32
33 IMPLEMENT_DYNCREATE (CGhostTextBuffer, CCrystalTextBuffer)
34
35 /**
36  * @brief Constructor.
37  */
38 CGhostTextBuffer::CGhostTextBuffer()
39 {
40 }
41
42 #if 0
43 /**
44  * @brief Insert a ghost line.
45  * @param [in] pSource View into which to insert the line.
46  * @param [in] nLine Line (apparent/screen) where to insert the ghost line.
47  * @return true if the insertion succeeded, false otherwise.
48  */
49 bool CGhostTextBuffer::InternalInsertGhostLine (CCrystalTextView * pSource,
50                 int nLine)
51 {
52         ASSERT (m_bInit);             //  Text buffer not yet initialized.
53         //  You must call InitNew() or LoadFromFile() first!
54
55         ASSERT (nLine >= 0 && nLine <= static_cast<intptr_t>(m_aLines.size ()));
56
57         CInsertContext context;
58         context.m_ptStart.x = 0;
59         context.m_ptStart.y = nLine;
60         context.m_ptEnd.x = 0;
61         context.m_ptEnd.y = nLine + 1;
62
63         CCrystalTextBuffer::InsertLine (_T(""), 0, nLine);
64         if (pSource != nullptr)
65                 UpdateViews (pSource, &context, UPDATE_HORZRANGE | UPDATE_VERTRANGE, nLine);
66
67         return true;
68 }
69 #endif
70
71 /** InternalDeleteGhostLine accepts only apparent line numbers */
72 /**
73  * @brief Delete a group of ghost lines.
74  * @param [in] pSource View from which to delete the lines.
75  * @param [in] nLine Line index where to delete the first ghost line.
76  * @param [in] nCount the number of ghost lines to delete
77  * @return true if the deletion succeeded, false otherwise.
78  * @note @p nLine must be an apparent line number (ghost lines added).
79  */
80 bool CGhostTextBuffer::InternalDeleteGhostLine (CCrystalTextView * pSource,
81                 int nLine, int nCount)
82 {
83         ASSERT (m_bInit);             //  Text buffer not yet initialized.
84         //  You must call InitNew() or LoadFromFile() first!
85         ASSERT (nCount >= 0);
86         ASSERT (nLine >= 0 && (nLine + nCount) <= static_cast<intptr_t>(m_aLines.size ()));
87
88         if (nCount == 0)
89                 return true;
90
91         for (int i = nLine ; i < nLine + nCount; i++)
92         {
93                 ASSERT ( (GetLineFlags(i) & LF_GHOST) != 0 );
94                 m_aLines[i].Clear();
95         }
96
97         vector<LineInfo>::iterator iterBegin = m_aLines.begin() + nLine;
98         vector<LineInfo>::iterator iterEnd = iterBegin + nCount;
99         m_aLines.erase(iterBegin, iterEnd);
100
101         if (pSource != nullptr)
102         {
103                 CDeleteContext context;
104                 context.m_ptStart.y = nLine;
105                 context.m_ptStart.x = 0;
106                 context.m_ptEnd.y = nLine + nCount;
107                 context.m_ptEnd.x = 0;
108
109                 if (nLine == GetLineCount())
110                         nLine--;
111                 // The last parameter is optimization  
112                 //   - don't recompute lines preceding the removed line.
113                 UpdateViews (pSource, &context, UPDATE_HORZRANGE | UPDATE_VERTRANGE,
114                                 nLine);
115         }
116
117         return true;
118 }
119
120 /**
121  * @brief Get text of specified lines (ghost lines will not contribute to text).
122  * 
123  * @param nCrlfStyle determines the EOL type in the returned buffer.
124  * If nCrlfStyle equals CRLFSTYLE::AUTOMATIC, we read the EOL from the line buffer
125  * 
126  * @note This function has its base in CrystalTextBuffer
127  * CrystalTextBuffer::GetTextWithoutEmptys() is for a buffer with no ghost lines.
128  * CrystalTextBuffer::GetText() returns text including ghost lines.
129  * These two base functions never read the EOL from the line buffer, they
130  * use CRLFSTYLE::DOS when nCrlfStyle equals CRLFSTYLE::AUTOMATIC.
131  */
132 void CGhostTextBuffer::                 /* virtual override */
133 GetTextWithoutEmptys(int nStartLine, int nStartChar, 
134                  int nEndLine, int nEndChar, 
135                  CString &text, CRLFSTYLE nCrlfStyle /*= CRLFSTYLE::AUTOMATIC */,
136                  bool bExcludeInvisibleLines /*= true*/) const
137 {
138         const size_t lines = m_aLines.size();
139         ASSERT(nStartLine >= 0 && nStartLine < static_cast<intptr_t>(lines));
140         ASSERT(nStartChar >= 0 && nStartChar <= GetLineLength(nStartLine));
141         ASSERT(nEndLine >= 0 && nEndLine < static_cast<intptr_t>(lines));
142         ASSERT(nEndChar >= 0 && nEndChar <= GetFullLineLength(nEndLine));
143         ASSERT(nStartLine < nEndLine || nStartLine == nEndLine && nStartChar <= nEndChar);
144         // some edit functions (copy...) should do nothing when there is no selection.
145         // assert to be sure to catch these 'do nothing' cases.
146 //      ASSERT(nStartLine != nEndLine || nStartChar != nEndChar);
147
148         // estimate size (upper bound)
149         int nBufSize = 0;
150         int i = 0;
151         for (i = nStartLine; i <= nEndLine; ++i)
152                 nBufSize += (GetFullLineLength(i) + 2); // in case we insert EOLs
153         LPTSTR pszBuf = text.GetBuffer(nBufSize);
154
155         if (nCrlfStyle != CRLFSTYLE::AUTOMATIC)
156         {
157                 // we must copy this EOL type only
158                 const CString sEol = GetStringEol (nCrlfStyle);
159
160                 for (i = nStartLine; i <= nEndLine; ++i)
161                 {
162                         // exclude ghost lines
163                         if ((GetLineFlags(i) & LF_GHOST) || (bExcludeInvisibleLines && (GetLineFlags(i) & LF_INVISIBLE)))
164                                 continue;
165
166                         // copy the line, excluding the EOL
167                         int soffset = (i == nStartLine ? nStartChar : 0);
168                         int eoffset = (i == nEndLine ? nEndChar : GetLineLength(i));
169                         int chars = eoffset - soffset;
170                         LPCTSTR szLine = m_aLines[i].GetLine(soffset);
171                         CopyMemory(pszBuf, szLine, chars * sizeof(TCHAR));
172                         pszBuf += chars;
173
174                         // copy the EOL of the requested type
175                         if (i != ApparentLastRealLine())
176                         {
177                                 CopyMemory(pszBuf, sEol, sEol.GetLength() * sizeof(TCHAR));
178                                 pszBuf += sEol.GetLength();
179                         }
180                 }
181         } 
182         else 
183         {
184                 for (i = nStartLine; i <= nEndLine; ++i)
185                 {
186                         // exclude ghost lines
187                         if ((GetLineFlags(i) & LF_GHOST) || (bExcludeInvisibleLines && GetLineFlags(i) & LF_INVISIBLE))
188                                 continue;
189
190                         // copy the line including the EOL
191                         int soffset = (i == nStartLine ? nStartChar : 0);
192                         int eoffset = (i == nEndLine ? nEndChar : GetFullLineLength(i));
193                         int chars = eoffset - soffset;
194                         LPCTSTR szLine = m_aLines[i].GetLine(soffset);
195                         CopyMemory(pszBuf, szLine, chars * sizeof(TCHAR));
196                         pszBuf += chars;
197
198                         // check that we really have an EOL
199                         if (i != ApparentLastRealLine() && GetLineLength(i) == GetFullLineLength(i))
200                         {
201                                 // Oops, real line lacks EOL
202                                 // (If this happens, editor probably has bug)
203                                 ASSERT(false);
204                                 CString sEol = GetStringEol (nCrlfStyle);
205                                 CopyMemory(pszBuf, sEol, sEol.GetLength() * sizeof(TCHAR));
206                                 pszBuf += sEol.GetLength();
207                         }
208                 }
209         }
210         text.ReleaseBuffer(static_cast<int>(pszBuf - text));
211         text.FreeExtra();
212 }
213
214 ////////////////////////////////////////////////////////////////////////////
215 // edition functions
216
217 /**
218  * @brief Insert text to the buffer.
219  * @param [in] pSource View into which to insert the text.
220  * @param [in] nLine Line number (apparent/screen) where the insertion starts.
221  * @param [in] nPos Character position where the insertion starts.
222  * @param [in] pszText The text to insert.
223  * @param [in] cchText The length of text in pszText.
224  * @param [out] nEndLine Line number of last added line in the buffer.
225  * @param [out] nEndChar Character position of the end of the added text
226  *   in the buffer.
227  * @param [in] nAction Edit action.
228  * @param [in] bHistory Save insertion for undo/redo?
229  * @return true if the insertion succeeded, false otherwise.
230  * @note Line numbers are apparent (screen) line numbers, not real
231  * line numbers in the file.
232  * @note @p nEndLine and @p nEndChar are valid as long as you do not call
233  *   FlushUndoGroup. If you need to call FlushUndoGroup, just store them in a
234  *   variable which is preserved with real line number during Rescan
235  *   (m_ptCursorPos, m_ptLastChange for example).
236  */
237 bool CGhostTextBuffer::                 /* virtual override */
238 InsertText (CCrystalTextView * pSource, int nLine,
239                 int nPos, LPCTSTR pszText, size_t cchText, int &nEndLine, int &nEndChar,
240                 int nAction /*= CE_ACTION_UNKNOWN*/, bool bHistory /*= true*/)
241 {
242         bool bGroupFlag = false;
243         bool bFirstLineGhost = ((GetLineFlags(nLine) & LF_GHOST) != 0);
244         bool bSpecialLastLineHandling = bFirstLineGhost && (nLine == GetLineCount()-1);
245
246         if (bFirstLineGhost && cchText > 0)
247         {
248                 CString text = GetStringEol(GetCRLFMode());
249                 if (bHistory && !m_bUndoGroup)
250                 {
251                         BeginUndoGroup();
252                         bGroupFlag = true;
253                 }
254                 auto reverseFindRealLine = [&](int nLine) {
255                         for (; nLine >= 0; --nLine) { if ((GetLineFlags(nLine) & LF_GHOST) == 0) break; }
256                         return nLine;
257                 };
258                 int i = reverseFindRealLine(nLine);
259                 if (i >= 0 && !m_aLines[i].HasEol())
260                         CCrystalTextBuffer::InsertText(pSource, i, GetLineLength(i), text, text.GetLength(), nEndLine, nEndChar, 0, bHistory);
261                 else if (!LineInfo::IsEol(pszText[cchText - 1]))
262                 {
263                         auto findRealLine = [&](int nLine) {
264                                 for (; nLine < GetLineCount(); ++nLine) { if ((GetLineFlags(nLine) & LF_GHOST) == 0) break; }
265                                 if (nLine == GetLineCount())
266                                         return -1;
267                                 return nLine;
268                         };
269                         if (findRealLine(nLine) != -1)
270                                 CCrystalTextBuffer::InsertText(pSource, nLine, 0, text, text.GetLength(), nEndLine, nEndChar, 0, bHistory);
271                 }
272         }
273
274         if (!CCrystalTextBuffer::InsertText (pSource, nLine, nPos, pszText,
275                 cchText, nEndLine, nEndChar, nAction, bHistory))
276         {
277                 return false;
278         }
279
280         // when inserting an EOL terminated text into a ghost line,
281         // there is a discrepancy between nInsertedLines and nEndLine-nRealLine
282         bool bDiscrepancyInInsertedLines;
283         if (bFirstLineGhost && nEndChar == 0 && ApparentLastRealLine() >= nEndLine)
284                 bDiscrepancyInInsertedLines = true;
285         else
286                 bDiscrepancyInInsertedLines = false;
287
288         if (bSpecialLastLineHandling)
289         {
290                 // The special case of inserting text into the very last line of a file when
291                 //      that last line is marked as LF_GHOST.  Effectively, the new text is 
292                 //      supposed to go "before" the Ghost, but mechanically the text is inserted
293                 //      into the Ghost itself, with a new Ghost line appearing at the end of the
294                 //      file.  Later (below), the Ghost status of both the first and last inserted 
295                 //      lines will get straightened out, with the trailing Ghost line becomming  
296                 //      a NULL line.
297                 if ((GetLineFlags(nLine) & LF_GHOST) != 0)      // first line still marked GHOST
298                         bSpecialLastLineHandling = false;
299                 else
300                         bDiscrepancyInInsertedLines = false;
301         }
302
303         // compute the number of real lines created (for undo)
304         int nRealLinesCreated = nEndLine - nLine;
305         if (bFirstLineGhost && (nEndChar > 0 || ApparentLastRealLine() < nEndLine))
306                 // we create one more real line
307                 nRealLinesCreated ++;
308
309         int i;
310         for (i = nLine ; i < nEndLine ; i++)
311         {
312                 // update line revision numbers of modified lines
313                 m_aLines[i].m_dwRevisionNumber = m_dwCurrentRevisionNumber;
314                 OnNotifyLineHasBeenEdited(i);
315         }
316         if (!bDiscrepancyInInsertedLines)
317         {
318                 m_aLines[i].m_dwRevisionNumber = m_dwCurrentRevisionNumber;
319                 OnNotifyLineHasBeenEdited(i);
320         }
321
322         // when inserting into a ghost line block, we want to replace ghost lines
323         // with our text, so delete some ghost lines below the inserted text
324         if (bFirstLineGhost)
325         {
326                 // where is the first line after the inserted text ?
327                 int nInsertedTextLinesCount = nEndLine - nLine + (bDiscrepancyInInsertedLines ? 0 : 1);
328                 int nLineAfterInsertedBlock = nLine + nInsertedTextLinesCount;
329                 // delete at most nInsertedTextLinesCount - 1 ghost lines
330                 // as the first ghost line has been reused
331                 int nMaxGhostLineToDelete = min(nInsertedTextLinesCount - 1, GetLineCount()-nLineAfterInsertedBlock);
332                 if (nEndChar == 0 && ApparentLastRealLine() < nEndLine)
333                         nMaxGhostLineToDelete --;
334                 for (i = 0 ; i < nMaxGhostLineToDelete ; i++)
335                         if ((GetLineFlags(nLineAfterInsertedBlock+i) & LF_GHOST) == 0)
336                                 break;
337                 InternalDeleteGhostLine(pSource, nLineAfterInsertedBlock, i);
338         }
339
340         for (i = nLine ; i < nEndLine ; i++)
341                 SetLineFlag (i, LF_GHOST, false, false, false);
342         if (!bDiscrepancyInInsertedLines)
343                 // if there is no discrepancy, the final cursor line is real
344                 // as either some text was inserted in it, or it inherits the real status from the first line
345                 SetLineFlag (i, LF_GHOST, false, false, false);
346         else
347                 // if there is a discrepancy, the final cursor line was not changed during insertion so we do nothing
348                 ;
349                 
350         if (bSpecialLastLineHandling)
351         {
352                 // By setting the last line (in this special case, see above) to `nullptr`, 
353                 //      the line will eventually be removed or become an actual LF_GHOST line.
354                 int nLastLine = GetLineCount()-1;
355                 ASSERT(m_aLines[nLastLine].FullLength() == 0);
356                 m_aLines[nLastLine].Clear();
357         }
358
359         // now we can recompute
360         if ((nEndLine > nLine) || bFirstLineGhost)
361         {
362                 // TODO: Be smarter, and don't recompute if it is easy to see what changed
363                 RecomputeRealityMapping();
364         }
365
366         if (bGroupFlag)
367                 FlushUndoGroup (pSource);
368
369         // nEndLine may have changed during Rescan
370         nEndLine = m_ptLastChange.y;
371
372         return true;
373 }
374
375 CDWordArray *CGhostTextBuffer::                 /* virtual override */
376 CopyRevisionNumbers(int nStartLine, int nEndLine) const
377 {
378         CDWordArray *paSavedRevisionNumbers = CCrystalTextBuffer::CopyRevisionNumbers(nStartLine, nEndLine);
379         for (int nLine = nEndLine; nLine >= nStartLine; --nLine)
380         {
381                 if ((GetLineFlags(nLine) & LF_GHOST) != 0)
382                         paSavedRevisionNumbers->RemoveAt(nLine - nStartLine);
383         }
384         if ((GetLineFlags(nEndLine) & LF_GHOST) != 0)
385         {
386                 for (int nLine = nEndLine + 1; nLine < GetLineCount(); ++nLine)
387                         if ((GetLineFlags(nLine) & LF_GHOST) == 0)
388                         {
389                                 paSavedRevisionNumbers->Add(GetLineFlags(nLine));
390                                 break;
391                         }
392         }
393         return paSavedRevisionNumbers;
394 }
395
396 void CGhostTextBuffer::                 /* virtual override */
397 RestoreRevisionNumbers(int nStartLine, CDWordArray *paSavedRevisionNumbers)
398 {
399         for (int i = 0, j = 0; i < paSavedRevisionNumbers->GetSize(); j++)
400         {
401                 if ((GetLineFlags(nStartLine + j) & LF_GHOST) == 0)
402                 {
403                         m_aLines[nStartLine + j].m_dwRevisionNumber = (*paSavedRevisionNumbers)[i];
404                         ++i;
405                 }
406         }
407 }
408
409 bool CGhostTextBuffer::                 /* virtual override */
410 DeleteText2 (CCrystalTextView * pSource, int nStartLine, int nStartChar,
411             int nEndLine, int nEndChar, int nAction /*= CE_ACTION_UNKNOWN*/, bool bHistory /*= true*/)
412 {
413         int const nLineCount = GetLineCount();
414         while (nEndLine < nLineCount - 1 && GetLineFlags(nEndLine) & LF_GHOST)
415                 ++nEndLine;
416         if (!CCrystalTextBuffer::DeleteText2(pSource, nStartLine, nStartChar,
417                 nEndLine, nEndChar, nAction, bHistory))
418         {
419                 return false;
420         }
421
422         if (nStartChar != 0 || nEndChar != 0)
423                 OnNotifyLineHasBeenEdited(nStartLine);
424
425         // now we can recompute
426         if (nStartLine != nEndLine)
427         {
428                 // TODO: Be smarter, and don't recompute if it is easy to see what changed
429                 RecomputeRealityMapping();
430         }
431                 
432         return true;
433 }
434
435 #if 0
436 /**
437  * @brief Insert a ghost line to the buffer (and view).
438  * @param [in] pSource The view to which to add the ghost line.
439  * @param [in] Line index (apparent/screen) where to add the ghost line.
440  * @return true if the addition succeeded, false otherwise.
441  */
442 bool CGhostTextBuffer::InsertGhostLine (CCrystalTextView * pSource, int nLine)
443 {
444         if (!InternalInsertGhostLine (pSource, nLine))
445                 return false;
446
447         // Set WinMerge flags  
448         SetLineFlag (nLine, LF_GHOST, true, false, false);
449         RecomputeRealityMapping();
450
451         // Don't need to recompute EOL as real lines are unchanged.
452         // Never AddUndoRecord as Rescan clears the ghost lines.
453         return true;
454 }
455 #endif
456
457 /**
458  * @brief Remove all the ghost lines from the buffer.
459  */
460 void CGhostTextBuffer::RemoveAllGhostLines()
461 {
462         int nlines = GetLineCount();
463         int nFirstGhost = -1;
464         // Free the buffer of ghost lines, 
465         // remember where the first ghost line occurs
466         for(int ct = 0; ct < nlines; ct++)
467         {
468                 if (GetLineFlags(ct) & LF_GHOST)
469                 {
470                         m_aLines[ct].FreeBuffer();
471                         if (nFirstGhost < 0)
472                                 nFirstGhost = ct;
473                 }
474         }
475         if (nFirstGhost >= 0)
476         {
477                 // Compact non-ghost lines, starting at the first ghost.
478                 // (we copy the buffer address, so the buffer doesn't move and we don't free it)
479                 int newnl = nFirstGhost;
480                 for (int ct = nFirstGhost; ct < nlines; ct++)
481                 {
482                         if ((GetLineFlags(ct) & LF_GHOST) == 0)
483                                 m_aLines[newnl++] = m_aLines[ct];
484                 }
485
486                 // Discard unused entries in one shot
487                 m_aLines.resize(newnl);
488                 RecomputeRealityMapping();
489         }
490 }
491
492 ////////////////////////////////////////////////////////////////////////////
493 // apparent <-> real line conversion
494
495 /**
496  * @brief Get last apparent (screen) line index.
497  * @return Last apparent line, or -1 if no lines in the buffer.
498  */
499 int CGhostTextBuffer::ApparentLastRealLine() const
500 {
501         if (m_RealityBlocks.size() == 0)
502                 return -1;
503         const RealityBlock &block = m_RealityBlocks.back();
504         return block.nStartApparent + block.nCount - 1;
505 }
506
507 /**
508  * @brief Get a real line for the apparent (screen) line.
509  * This function returns the real line for the given apparent (screen) line.
510  * For ghost lines we return next real line. For trailing ghost line we return
511  * last real line + 1). Ie, lines 0->0, 1->2, 2->4, for argument of 3,
512  * return 2.
513  * @param [in] nApparentLine Apparent line for which to get the real line.
514  * @return The real line for the apparent line.
515  */
516 int CGhostTextBuffer::ComputeRealLine(int nApparentLine) const
517 {
518         int decToReal;
519         return ComputeRealLineAndGhostAdjustment(nApparentLine, decToReal);
520 }
521
522 /**
523  * @brief Get an apparent (screen) line for the real line.
524  * @param [in] nRealLine Real line for which to get the apparent line.
525  * @return The apparent line for the real line. If real line is out of bounds
526  *   return last valid apparent line + 1.
527  */
528 int CGhostTextBuffer::ComputeApparentLine(int nRealLine) const
529 {
530         const int size = static_cast<int>(m_RealityBlocks.size());
531         if (size == 0)
532                 return 0;
533
534         // after last block ?
535         const RealityBlock & maxblock = m_RealityBlocks.back();
536         if (nRealLine >= maxblock.nStartReal + maxblock.nCount)
537                 return GetLineCount();
538
539         // binary search to find correct (or nearest block)
540         int blo = 0;
541         int bhi = size - 1;
542         while (blo <= bhi)
543         {
544                 int i = (blo + bhi) / 2;
545                 const RealityBlock & block = m_RealityBlocks[i];
546                 if (nRealLine < block.nStartReal)
547                         bhi = i - 1;
548                 else if (nRealLine >= block.nStartReal + block.nCount)
549                         blo = i + 1;
550                 else
551                         return (nRealLine - block.nStartReal) + block.nStartApparent;
552         }
553         // Should have found it; all real lines should be in a block
554         ASSERT(false);
555         return -1;
556 }
557
558 /**
559  * @brief Get a real line for apparent (screen) line.
560  * This function returns the real line for the given apparent (screen) line.
561  * For ghost lines we return next real line. For trailing ghost line we return
562  * last real line + 1; i.e. lines 0->0, 1->2, 2->4, for argument of 3,
563  * return 2 and decToReal would be 1.
564  * @param [in] nApparentLine Apparent line for which to get the real line.
565  * @param [out] decToReal Difference of the apparent and real line.
566  * @return The real line for the apparent line.
567  */
568 int CGhostTextBuffer::ComputeRealLineAndGhostAdjustment(int nApparentLine,
569                 int& decToReal) const
570 {
571         const int size = static_cast<int>(m_RealityBlocks.size());
572         if (size == 0) 
573         {
574                 decToReal = 0;
575                 return 0;
576         }
577
578         // after last apparent line ?
579         ASSERT(nApparentLine < GetLineCount());
580
581         // after last block ?
582         const RealityBlock & maxblock = m_RealityBlocks.back();
583         if (nApparentLine >= maxblock.nStartApparent + maxblock.nCount)
584         {
585                 decToReal = GetLineCount() - nApparentLine;
586                 return maxblock.nStartReal + maxblock.nCount;
587         }
588
589         // binary search to find correct (or nearest block)
590         int blo = 0;
591         int bhi = size - 1;
592         while (blo <= bhi)
593         {
594                 int i = (blo + bhi) / 2;
595                 const RealityBlock & block = m_RealityBlocks[i];
596                 if (nApparentLine < block.nStartApparent)
597                         bhi = i - 1;
598                 else if (nApparentLine >= block.nStartApparent + block.nCount)
599                         blo = i + 1;
600                 else // found it inside this block
601                 {
602                         decToReal = 0;
603                         return (nApparentLine - block.nStartApparent) + block.nStartReal;
604                 }
605         }
606         // it is a ghost line just before block blo
607         decToReal = m_RealityBlocks[blo].nStartApparent - nApparentLine;
608         return m_RealityBlocks[blo].nStartReal;
609 }
610
611 /**
612  * @brief Get an apparent (screen) line for the real line.
613  * @param [in] nRealLine Real line for which to get the apparent line.
614  * @param [in] decToReal Difference of the apparent and real line.
615  * @return The apparent line for the real line. If real line is out of bounds
616  *   return last valid apparent line + 1.
617  */
618 int CGhostTextBuffer::ComputeApparentLine(int nRealLine, int decToReal) const
619 {
620         int nPreviousBlock;
621         int nApparent;
622
623         const int size = (int) m_RealityBlocks.size();
624         int blo = 0;
625         int bhi = size - 1;
626         int i;
627         if (size == 0)
628                 return 0;
629
630         // after last block ?
631         const RealityBlock & maxblock = m_RealityBlocks.back();
632         if (nRealLine >= maxblock.nStartReal + maxblock.nCount)
633         {
634                 nPreviousBlock = size - 1;
635                 nApparent = GetLineCount() - 1;
636                 goto limitWithPreviousBlock;
637         }
638
639         // binary search to find correct (or nearest block)
640         while (blo <= bhi)
641         {
642                 i = (blo + bhi) / 2;
643                 const RealityBlock & block = m_RealityBlocks[i];
644                 if (nRealLine < block.nStartReal)
645                         bhi = i - 1;
646                 else if (nRealLine >= block.nStartReal + block.nCount)
647                         blo = i + 1;
648                 else
649                 {
650                         if (nRealLine > block.nStartReal)
651                                 // limited by the previous line in this block
652                                 return (nRealLine - block.nStartReal) + block.nStartApparent;
653                         nPreviousBlock = i - 1;
654                         nApparent = (nRealLine - block.nStartReal) + block.nStartApparent;
655                         goto limitWithPreviousBlock;
656                 }
657         }
658         // Should have found it; all real lines should be in a block
659         ASSERT(false);
660         return -1;
661
662 limitWithPreviousBlock:
663         // we must keep above the value lastApparentInPreviousBlock
664         int lastApparentInPreviousBlock;
665         if (nPreviousBlock == -1)
666                 lastApparentInPreviousBlock = -1;
667         else
668         {
669                 const RealityBlock & previousBlock = m_RealityBlocks[nPreviousBlock];
670                 lastApparentInPreviousBlock = previousBlock.nStartApparent + previousBlock.nCount - 1;
671         }
672
673         while (decToReal --) 
674         {
675                 nApparent --;
676                 if (nApparent == lastApparentInPreviousBlock)
677                         return nApparent + 1;
678         }
679         return nApparent;
680 }
681
682 /** Do what we need to do just after we've been reloaded */
683 void CGhostTextBuffer::FinishLoading()
684 {
685         if (!m_bInit) return;
686         RecomputeRealityMapping();
687 }
688
689 /** Recompute the reality mapping (this is fairly naive) */
690 void CGhostTextBuffer::RecomputeRealityMapping()
691 {
692         m_RealityBlocks.clear();
693         int reality = -1; // last encountered real line
694         int i = 0; // current line
695         int nLineCount = GetLineCount();
696         RealityBlock block; // current block being traversed (in state 2)
697
698         // This is a state machine with 2 states
699
700         // state 1, i-1 not real line
701 passingGhosts:
702         ASSERT( i <= nLineCount );
703         if (i == nLineCount)
704         {
705                 checkFlagsFromReality();
706                 return;
707         }
708         if (GetLineFlags(i) & LF_GHOST)
709         {
710                 ++i;
711                 goto passingGhosts;
712         }
713         // this is the first line of a reality block
714         block.nStartApparent = i;
715         block.nStartReal = reality + 1;
716         block.nCount = -1;
717         ++reality;
718         ++i;
719         // fall through to other state
720
721         // state 2, i - 1 is real line
722 inReality:
723         ASSERT( i <= nLineCount );
724         if (i == nLineCount || (GetLineFlags(i) & LF_GHOST))
725         {
726                 // i-1 is the last line of a reality block
727                 ASSERT(reality >= 0);
728                 block.nCount = i - block.nStartApparent;
729                 ASSERT(block.nCount > 0);
730                 ASSERT(reality + 1 - block.nStartReal == block.nCount);
731                 
732                 // Optimize memory allocation
733                 if (m_RealityBlocks.capacity() == m_RealityBlocks.size())
734                 {
735                         if (m_RealityBlocks.size() == 0)
736                                 m_RealityBlocks.reserve(16);
737                         else
738                                 // TODO: grow more slowly with really large RealityBlocks
739                                 m_RealityBlocks.reserve(m_RealityBlocks.size() * 2);
740                 }
741                 m_RealityBlocks.push_back(block);
742                 if (i == nLineCount)
743                 {
744                         checkFlagsFromReality();
745                         return;
746                 }
747                 ++i;
748                 goto passingGhosts;
749         }
750         ++reality;
751         ++i;
752         goto inReality;
753 }
754
755 /** 
756 Check all lines, and ASSERT if reality blocks differ from flags. 
757 This means that this only has effect in DEBUG build
758 */
759 void CGhostTextBuffer::checkFlagsFromReality() const
760 {
761 #ifdef _DEBUG
762         const int size = static_cast<int>(m_RealityBlocks.size());
763         int i = 0;
764         for (int b = 0 ; b < size ; b ++)
765         {
766                 const RealityBlock & block = m_RealityBlocks[b];
767                 for ( ; i < block.nStartApparent ; i++)
768                         ASSERT ((GetLineFlags(i) & LF_GHOST) != 0);
769                 for ( ; i < block.nStartApparent+block.nCount ; i++)
770                         ASSERT ((GetLineFlags(i) & LF_GHOST) == 0);
771         }
772
773         for ( ; i < GetLineCount() ; i++)
774                 ASSERT ((GetLineFlags(i) & LF_GHOST) != 0);
775 #endif 
776 }
777
778 void CGhostTextBuffer::                 /* virtual base */
779 OnNotifyLineHasBeenEdited(int nLine)
780 {
781         return;
782 }
783
784 void CGhostTextBuffer::
785 CountEolAndLastLineLength(const CPoint& ptStartPos, LPCTSTR pszText, size_t cchText, int &nLastLineLength, int &nEol)
786 {
787         nLastLineLength = 0;
788         nEol = 0;
789         if (m_bTableEditing && m_bAllowNewlinesInQuotes)
790         {
791                 bool bInQuote = false;
792                 const TCHAR* pszLine = m_aLines[ptStartPos.y].GetLine();
793                 for (int j = 0; j < ptStartPos.x; ++j)
794                 {
795                         if (pszLine[j] == m_cFieldEnclosure)
796                                 bInQuote = !bInQuote;
797                 }
798                 for (size_t nTextPos = 0; nTextPos < cchText; ++nTextPos)
799                 {
800                         if (pszText[nTextPos] == m_cFieldEnclosure)
801                                 bInQuote = !bInQuote;
802                         if (!bInQuote && LineInfo::IsEol(pszText[nTextPos]))
803                         {
804                                 if (nTextPos + 1 < cchText && LineInfo::IsDosEol(&pszText[nTextPos]))
805                                         ++nTextPos;
806                                 ++nEol;
807                                 nLastLineLength = 0;
808                         }
809                         else
810                                 ++nLastLineLength;
811                 }
812         }
813         else
814         {
815                 for (size_t nTextPos = 0; nTextPos < cchText; ++nTextPos)
816                 {
817                         if (LineInfo::IsEol(pszText[nTextPos]))
818                         {
819                                 if (nTextPos + 1 < cchText && LineInfo::IsDosEol(&pszText[nTextPos]))
820                                         ++nTextPos;
821                                 ++nEol;
822                                 nLastLineLength = 0;
823                         }
824                         else
825                                 ++nLastLineLength;
826                 }
827         }
828 }
829
830 void CGhostTextBuffer::                 /* virtual override */
831 AddUndoRecord(bool bInsert, const CPoint & ptStartPos,
832         const CPoint & ptEndPos, LPCTSTR pszText, size_t cchText,
833         int nActionType /*= CE_ACTION_UNKNOWN*/,
834         CDWordArray *paSavedRevisionNumbers /*= nullptr*/)
835 {
836         CPoint real_ptStartPos(ptStartPos.x, ComputeRealLine(ptStartPos.y));
837         int nLastLineLength, nEol;
838         CountEolAndLastLineLength(ptStartPos, pszText, cchText, nLastLineLength, nEol);
839         CPoint real_ptEndPos(ptEndPos.x, real_ptStartPos.y + nEol);
840         if (ptEndPos.x == 0 && cchText > 0 && !LineInfo::IsEol(pszText[cchText - 1]))
841                 real_ptEndPos.x = nLastLineLength;
842         CCrystalTextBuffer::AddUndoRecord(bInsert, real_ptStartPos, real_ptEndPos, pszText,
843                 cchText, nActionType, paSavedRevisionNumbers);
844 }
845
846 UndoRecord CGhostTextBuffer::                   /* virtual override */
847 GetUndoRecord(int nUndoPos) const
848 {
849         UndoRecord ur = m_aUndoBuf[nUndoPos];
850         ur.m_ptStartPos.y = ComputeApparentLine(ur.m_ptStartPos.y, 0);
851         ur.m_ptEndPos.y = ComputeApparentLine(ur.m_ptEndPos.y, 0);
852         return ur;
853 }
854
855 bool CGhostTextBuffer::         /* virtual override */
856 UndoInsert(CCrystalTextView * pSource, CPoint & ptCursorPos, const CPoint apparent_ptStartPos, CPoint const apparent_ptEndPos, const UndoRecord & ur)
857 {    
858     // Check that text in the undo buffer matches text in file buffer.  
859         // If not, then rescan() has moved lines and undo fails.
860
861     // we need to put the cursor before the deleted section
862     CString text;
863     const size_t size = m_aLines.size();
864     if ((apparent_ptStartPos.y < static_cast<LONG>(size)) &&
865         (apparent_ptStartPos.x <= static_cast<LONG>(m_aLines[apparent_ptStartPos.y].Length())) &&
866         (apparent_ptEndPos.y < static_cast<LONG>(size)) &&
867         (apparent_ptEndPos.x <= static_cast<LONG>(m_aLines[apparent_ptEndPos.y].Length())))
868     {
869                 //  Try to ensure that we are undoing correctly...
870                 //  Just compare the text as it was before Undo operation
871         GetTextWithoutEmptys (apparent_ptStartPos.y, apparent_ptStartPos.x, apparent_ptEndPos.y, apparent_ptEndPos.x, text, CRLFSTYLE::AUTOMATIC, false);
872         if (static_cast<size_t>(text.GetLength()) == ur.GetTextLength() && memcmp(text, ur.GetText(), text.GetLength() * sizeof(TCHAR)) == 0)
873         {
874                         if (CCrystalTextBuffer::UndoInsert(pSource, ptCursorPos, apparent_ptStartPos, apparent_ptEndPos, ur))
875                         {
876                                 // ptCursorPos = apparent_ptStartPos;
877                                 return true;
878                         }
879         }
880                 else
881                 // It is possible that the ptEndPos is at the last line of the file and originally pointed
882                 //      at an LF_GHOST line that followed (and has since been discarded).  Lets try to reconstruct
883                 //      that situation before we fail entirely...
884                 if (apparent_ptEndPos.y + 1 == static_cast<LONG>(size) && apparent_ptEndPos.x == 0)
885                 {
886                         CPoint apparentEnd2 = apparent_ptEndPos;
887                         apparentEnd2.x = static_cast<LONG>(m_aLines[apparentEnd2.y].FullLength());
888                         text.Empty();
889                         GetTextWithoutEmptys(apparent_ptStartPos.y, apparent_ptStartPos.x, apparent_ptEndPos.y, apparentEnd2.x, text, CRLFSTYLE::AUTOMATIC, false);
890                         if (static_cast<size_t>(text.GetLength()) == ur.GetTextLength() && memcmp(text, ur.GetText(), text.GetLength() * sizeof(TCHAR)) == 0)
891                         {
892                                 if (CCrystalTextBuffer::UndoInsert(pSource, ptCursorPos, apparent_ptStartPos, apparentEnd2, ur))
893                                 {
894                                         // ptCursorPos = apparent_ptStartPos;
895                                         const size_t nLastLine = m_aLines.size() - 1;
896                                         if (m_aLines[nLastLine].Length() == 0)
897                                         {
898                                                 m_aLines[nLastLine].Clear();
899                                                 if (static_cast<size_t>(ptCursorPos.y) == nLastLine)
900                                                         ptCursorPos.y--;
901                                         }
902                                         return true;
903                                 }
904                         }
905                 }
906     }
907         ASSERT(false);
908         return false;
909 }
910