/* ====================================================================
 * Copyright (c) 2003-2006, Martin Hauner
 *                          http://subcommander.tigris.org
 *
 * Subcommander is licensed as described in the file doc/COPYING, which
 * you should have received as part of this distribution.
 * ====================================================================
 */

// sc
#include "TextModelImpl.h"
#include "Cursor.h"
#include "Line.h"
#include "Tab.h"
#include "util/String.h"

// sys
#include <assert.h>
#include <string.h>
#include <algorithm>
#include <map>
#include <list>
#include "util/max.h"

///////////////////////////////////////////////////////////////////////////////

class TextCmd
{
public:
  virtual ~TextCmd() {}

  virtual void run() = 0;
  virtual void undo() = 0;
};

///////////////////////////////////////////////////////////////////////////////

typedef std::list< Line >                        Lines;
typedef std::map< svn::Offset, Lines::iterator > LineNrs;

typedef std::list< TextCmd* >                    Cmds;



class TextModelImpl::Member
{
public:
  // \todo setTabWidth()
  Member() : _lineEnds(leNone), _tabWidth(2), _lastCol(0), _maxCol(0) 
    //, maxLine(0) 
  {
    _cmdIt = _cmds.end();
  }

  ~Member()
  {
    for( Cmds::iterator it = _cmds.begin(); it != _cmds.end(); it++ )
    {
      delete (*it);
    }
  }

  LineEnd getLineEnd()
  {
    return _lineEnds;
  }

  LineEnd checkLineEnd( const Line& line )
  {
    if( _lineEnds == leNone )
    {
      sc::Size bytes = line.getBytes();

      if( bytes >= 1 && line.getStr()[bytes-1] == '\r' )
      {
        _lineEnds = leCR;
      }
      else if( bytes >= 1 && line.getStr()[bytes-1] == '\n' )
      {
        _lineEnds = leLF;

        if( bytes >= 2 && line.getStr()[bytes-2] == '\r' )
        {
          _lineEnds = leCRLF;
        }
      }
    }
    return _lineEnds;
  }

  const char* getLineEndStr()
  {
    if( _lineEnds == leLF )
    {
      return sLF;
    }
    else if( _lineEnds == leCR )
    {
      return sCR;
    }
    else if( _lineEnds == leCRLF )
    {
      return sCRLF;
    }
    else
    {
      // \todo default value
      return sLF;
    }
  }

  Lines::iterator addLine( const Line& line )
  {
    Lines::iterator it = _lines.insert( _lines.end(), line );
    _lineNrs.insert( LineNrs::value_type(_lineNrs.size(),it) );

    Tab tab(_tabWidth);
    _maxCol = std::max( _maxCol, (size_t)tab.calcColumns(line.getStr()) );

    return it;
  }

  Lines::iterator getLine( svn::Offset lineNr )
  {
    LineNrs::iterator it = _lineNrs.find( lineNr );
    if( it == _lineNrs.end() )
    {
      return _lines.end();
    }
    return (*it).second;
  }

  Lines::iterator getLine()
  {
    return getLine( _cursor.line() );
  }

  const Line& getLine( Lines::iterator it )
  {
    if( it == _lines.end() )
    {
      return Line::getEmpty();
    }
    return *it;
  }

  bool nextLine( const Cursor& c, int& down )
  {
    down = 0;

    for( Lines::iterator it = getLine( c.line()+1 ); it != _lines.end(); it++ )
    {
      down++;

      if( ! (*it).isEmpty() )
      {
        return true;
      }
    }
    return false;
  }

  bool prevLine( const Cursor& c, int& up )
  {
    up = 0;

    // since we move up check against begin and end (not found)
    for( Lines::iterator it = getLine( c.line()-1 );
         it != _lines.begin() && it != _lines.end(); it-- )
    {
      up++;

      if( ! (*it).isEmpty() )
      {
        return true;
      }
    }
    return false;
  }

  Cursor calcNearestCursorPos( const Cursor& c )
  {
    Cursor nc = c;

    Lines::iterator it = getLine( c.line() );

    if( it == _lines.end() )
    {
      // cursor is in empty space below all files
      nc.setLine((int)(_lines.size()-1));
    }
    else if( (*it).isEmpty() )
    {
      int up;
      int down;
      /*bool bup   =*/ prevLine( nc, up );
      /*bool bdown =*/ nextLine( nc, down );

      if( up <= down )
      {
        nc.up(up);
      }
      else
      {
        nc.down(down);
      }
    }

    const Line& l = getLine( getLine(nc.line()) );

    Tab tab(_tabWidth);
    nc.maxColumn( tab.calcColumns(l.getStr()) );
    nc.setColumn( tab.calcColumnForColumn(l.getStr(),nc.column()) );

    return nc;
  }

  Cursor moveCursorRight( bool moveC2 )
  {
    Cursor nc = _cursor;
    nc.setOn();

    // find next line, this may be more than 1 line down if we step
    // over "nop" lines.
    int  down  = 0;
    bool bdown = nextLine(nc,down);

    const Line& l = getLine( getLine(nc.line()) );

    Tab tab(_tabWidth);
    int curcol    = nc.column();
    int nextcol   = tab.calcColumnForNextChar( l.getStr(), curcol );
    int linewidth = tab.calcColumns( l.getStr() );

    // cursor right DOES change the line
    // if we check bdown we can't 'entf' the last char in the last line
    if( bdown && (curcol == linewidth) )
    {
      nc.down(down);
      nc.setColumn(0);
    }
    else // cursor right DOES NOT change the line
    {
      // move cursor right but never behind the last column in the current line,
      // this is (only) necessary if we are on the last possible line
      nc.setColumn( nextcol );
      nc.maxColumn( linewidth );
    }

    _lastCol = nc.column();
    _cursor  = nc;
    if( moveC2 )
    {
      _cursor2 = _cursor;
    }
    return nc;
  }

  Cursor moveCursorLeft( bool moveC2 )
  {
    Cursor nc = _cursor;
    nc.setOn();

    // find previous line
    int  up  = 0;
    bool bup = prevLine(nc,up);

    // already leftmost cursor position?
    if( ! bup && nc.column() == 0 )
    {
      // yes
      return nc;
    }

    const Line& l  = getLine( getLine( nc.line() ) );
    const Line& lp = getLine( getLine( nc.line()-up ) );

    Tab tab(_tabWidth);
    int curcol     = nc.column();
    int prevcol    = tab.calcColumnForPrevChar( l.getStr(), nc.column() );
    int linewidthp = tab.calcColumns( lp.getStr() );

    // cursor left DOES change the line
    if( (prevcol == 0) && (curcol == 0) )
    {
      nc.up(up);
      nc.setColumn( linewidthp );
    }
    // cursor left DOES NOT change the line
    else
    {
      nc.setColumn(prevcol);
      nc.minColumn(0);
    }

    _lastCol = nc.column();
    _cursor  = nc;
    if( moveC2 )
    {
      _cursor2 = _cursor;
    }
    return nc;
  }

  Cursor moveCursorDown( bool moveC2 )
  {
    Cursor nc = _cursor;
    nc.setOn();

    // find next line
    int  down  = 0;
    bool bdown = nextLine(nc,down);
    if( ! bdown )
    {
      return nc;
    }

    const Line& l = getLine( getLine( nc.line()+down ) );

    Tab tab(_tabWidth);
    int linewidth = tab.calcColumns( l.getStr() );

    nc.down(down);

    if( _lastCol < linewidth )
    {
      nc.setColumn(_lastCol);
    }
    else
    {
      nc.setColumn(linewidth);
    }

    _cursor = nc;
    if( moveC2 )
    {
      _cursor2 = _cursor;
    }
    return nc;
  }

  Cursor moveCursorUp( bool moveC2 )
  {
    Cursor nc = _cursor;
    nc.setOn();

    // find previous line
    int  up  = 0;
    bool bup = prevLine(nc,up);
    if( ! bup )
    {
      return nc;
    }

    const Line& l = getLine( getLine( nc.line()-up ) );

    Tab tab(_tabWidth);
    int linewidth = tab.calcColumns( l.getStr() );

    nc.up(up);

    if( _lastCol < linewidth )
    {
      nc.setColumn(_lastCol);
    }
    else
    {
      nc.setColumn(linewidth);
    }

    _cursor = nc;
    if( moveC2 )
    {
      _cursor2 = _cursor;
    }
    return nc;
  }

  void addCmd( TextCmd* cmd )
  {
    for( Cmds::iterator it = _cmdIt; it != _cmds.end(); )
    {
      delete (*it);
      it = _cmds.erase(it);
    }

    _cmds.push_back( cmd );
    _cmdIt = _cmds.end();
  }

  TextCmd* getUndo()
  {
    if( _cmdIt == _cmds.begin() )
    {
      return 0;
    }
    return *(--_cmdIt);
  }

  TextCmd* getRedo()
  {
    if( _cmdIt == _cmds.end() )
    {
      return 0;
    }

    return *(_cmdIt++);
  }

  void calcMaxColumn()
  {
    Tab tab(_tabWidth);

    _maxCol = 0;
    for( Lines::iterator it = _lines.begin(); it != _lines.end(); it++ )
    {
      _maxCol = std::max( _maxCol, (size_t)tab.calcColumns((*it).getStr()) );
    }
  }

  void rebuildLineNrs()
  {
    _lineNrs.clear();

    svn::Offset cnt = 0;
    for( Lines::iterator it = _lines.begin(); it != _lines.end(); it++, cnt++ )
    {
      _lineNrs.insert( LineNrs::value_type(cnt,it) );
    }
  }

  void setCursor( const Cursor& c, bool moveC2 )
  {
    _cursor = c;
    if( moveC2 )
    {
      _cursor2 = _cursor;
    }
  }

public:
  sc::String     _sourceName;
  LineEnd        _lineEnds;
  int            _tabWidth;

  Cursor         _cursor;
  Cursor         _cursor2;
  int            _lastCol;  // last column for cursor up/down

  Lines          _lines;
  LineNrs        _lineNrs;

  size_t         _maxCol;
  //size_t         maxLine;

  Cmds           _cmds;
  Cmds::iterator _cmdIt;
};

///////////////////////////////////////////////////////////////////////////////
//

class CompositeTextCmd : public TextCmd
{
public:
  CompositeTextCmd( const Cmds& cmds ) : _cmds(cmds)
  {
  }

  void run()
  {
    for( Cmds::iterator it = _cmds.begin(); it != _cmds.end(); it++ )
    {
      (*it)->run();
    }
  }

  void undo()
  {
    for( Cmds::reverse_iterator it = _cmds.rbegin(); it != _cmds.rend(); it++ )
    {
      (*it)->undo();
    }
  }

private:
  Cmds _cmds;
};

//
///////////////////////////////////////////////////////////////////////////////
//

// The input text can be a string with more than one character but without line
// feed. line feeds will come as a string that only contains the line feed.

class AddTextCmd : public TextCmd
{
public:
  AddTextCmd( TextModelImpl::Member* m, const sc::String& text )
    : M(m), _text(text)
  {
  }

  void run()
  {
    // store current cursor position
    _cursor = M->_cursor;

    Lines::iterator it = M->getLine( _cursor.line() );

    if( it == M->_lines.end() )
    {
      it = M->addLine( Line::getEmpty2() );
    }

    Line& line = *it;

    Tab tab(M->_tabWidth);
    int choff = tab.calcCharOffsetForColumn( line.getStr(), _cursor.column() );

    sc::String src = line.getLine();
    sc::String dst = src.left( choff );

    if( isReturn() )
    {
      dst += M->getLineEndStr();

      sc::String dst2;
      dst2 += src.right( src.getCharCnt() - choff );

      Line l  = Line( dst, line.getBlockNr(), line.getType() );
      Line l2 = Line( dst2, line.getBlockNr(), line.getType() );

      *it = l;
      M->_lines.insert( ++it, l2 );

      M->rebuildLineNrs();
      M->moveCursorRight(true);
    }
    else
    {
      dst += _text;
      dst += src.right( src.getCharCnt() - choff );

      Line l = Line( dst, line.getBlockNr(), line.getType() );
      *it    = l;

      for( int r = 0; r < (int)_text.getCharCnt(); r++ )
      {
        M->moveCursorRight(true);
      }
    }

    M->calcMaxColumn();
  }

  void undo()
  {
    Lines::iterator it = M->getLine( _cursor.line() );
    Line& line         = *it;

    Tab tab(M->_tabWidth);
    int choff = tab.calcCharOffsetForColumn( line.getStr(), _cursor.column() );

    sc::String src = line.getLine();
    sc::String dst = src.left( choff );

    if( isReturn() )
    {
      Lines::iterator itn = M->getLine( _cursor.line()+1 );
      Line& linen         = *itn;

      dst += linen.getStr();

      M->_lines.erase(itn);
      M->rebuildLineNrs();
    }
    else
    {
      dst += src.right( src.getCharCnt() - _text.getCharCnt() - choff );
    }

    Line l = Line( dst, line.getBlockNr(), line.getType() );
    *it    = l;

    M->calcMaxColumn();
    M->setCursor( _cursor, true );
  }

  bool isReturn()
  {
    static sc::String LF(sLF);
    static sc::String CR(sCR);
    static sc::String CRLF(sCRLF);

    if( _text == LF )
    {
      return true;
    }
    else if( _text == CR )
    {
      return true;
    }
    else if( _text == CRLF )
    {
      return true;
    }
    else
    {
      return false;
    }
  }

private:
  TextModelImpl::Member* M;
  Cursor                 _cursor;
  sc::String             _text;
};

//
///////////////////////////////////////////////////////////////////////////////
//

class RemoveTextLeftCmd : public TextCmd
{
public:
  RemoveTextLeftCmd( TextModelImpl::Member* m )
    : M(m), _cursor(m->_cursor)
  {
  }

  void run()
  {
    if( _cursor.equalPos(Cursor(0,0)) )
    {
      return;
    }

    Lines::iterator it = M->getLine( _cursor.line() );

    if( it == M->_lines.end() )
    {
      return;
    }

    Line& line = *it;

    Tab tab(M->_tabWidth);
    int choff = tab.calcCharOffsetForColumn( line.getStr(), _cursor.column() );

    if( choff == 0 )
    {
      Lines::iterator itp = M->getLine( _cursor.line()-1 );
      Line& linep         = *itp;

      int lf = 0;
      sc::String s;
      
      s = linep.getLine().right(1);
      if( s == sc::String(sCR) )
      {
        lf = 1;
      }
      else if( s == sc::String(sLF) )
      {
        lf = 1;
      }
      
      s = linep.getLine().right(2);
      if( s == sc::String(sCRLF) )
      {
        lf = 2;
      }

      int len = (int)linep.getLine().getCharCnt()-lf;
      _text = linep.getLine().right( lf );
      sc::String p;
      p += linep.getLine().left( len );
      p += line.getLine();

      Line l = Line( p, linep.getBlockNr(), linep.getType() );
      *itp   = l;

      M->_lines.erase(it);
      M->rebuildLineNrs();

      _cursor = Cursor( _cursor.line()-1, len );
      M->setCursor( _cursor, true );

      _removedLine = true;
    }
    else
    {
      M->moveCursorLeft(true);

      sc::String src = line.getLine();
      sc::String dst;
      dst += src.left( choff-1 );
      dst += src.right( src.getCharCnt() - choff );
      _text = src.mid( choff-1, 1 );

      Line l = Line( dst, line.getBlockNr(), line.getType() );
      *it    = l;

      _removedLine = false;
    }

    M->calcMaxColumn();
  }

  void undo()
  {
    Lines::iterator it = M->getLine( _cursor.line() );

    if( it == M->_lines.end() )
    {
      return;
    }

    Line& line = *it;

    Tab tab(M->_tabWidth);
    int choff = tab.calcCharOffsetForColumn( line.getStr(), _cursor.column() );

    if( _removedLine )
    {
      sc::String src = line.getLine();
      sc::String dst;
      dst += src.left( choff );
      dst += _text;

      sc::String dst2;
      dst2 += src.right( src.getCharCnt() - choff );

      Line l  = Line( dst, line.getBlockNr(), line.getType() );
      Line l2 = Line( dst2, line.getBlockNr(), line.getType() );

      *it = l;
      M->_lines.insert( ++it, l2 );

      M->rebuildLineNrs();

      _cursor = Cursor( _cursor.line()+1, 0 );
    }
    else
    {
      sc::String src = line.getLine();
      sc::String dst;
      dst += src.left( choff-1 );
      dst += _text;
      dst += src.right( src.getCharCnt() - (choff - 1) );

      Line l = Line( dst, line.getBlockNr(), line.getType() );
      *it    = l;
    }

    M->calcMaxColumn();
    M->setCursor( _cursor, true );
  }

protected:
  TextModelImpl::Member* M;
  Cursor                 _cursor;
  bool                   _removedLine;
  sc::String             _text;
};

//
///////////////////////////////////////////////////////////////////////////////
//

class RemoveTextRightCmd : public RemoveTextLeftCmd
{
public:
  RemoveTextRightCmd( TextModelImpl::Member* m )
    : RemoveTextLeftCmd( m )
  {
  }

  void run()
  {
    _cursor = M->moveCursorRight(true);

    RemoveTextLeftCmd::run();
  }

  void undo()
  {
    RemoveTextLeftCmd::undo();
    
    _cursor = M->moveCursorLeft(true);
  }
};

//
///////////////////////////////////////////////////////////////////////////////
//

class TextCmdFactory
{
public:
  static TextCmd* createAddTextCmd( TextModelImpl::Member* m, const sc::String& text )
  {
    Cmds cmds;

    const char* start = text.getStr();
    const char* curr  = start;

    while( *curr != 0 )
    {
      if( *curr == '\r' || *curr == '\n' )
      {
        // add a normal string if we have something to add
        if( curr > start )
        {
          sc::String part( start, curr-start );
          TextCmd* cmdPart = new AddTextCmd( m, part );
          cmds.push_back(cmdPart);
        }

        char lineEnd[2] = {};
        lineEnd[0] = *curr;
        curr++;

        // check if it is a CRLF
        if( *curr != 0 && lineEnd[0] == '\r' && *curr == '\n' )
        {
          lineEnd[1] = *curr;
          curr++;
        }

        sc::String le( lineEnd );
        TextCmd* cmdLE = new AddTextCmd( m, le );
        cmds.push_back(cmdLE);


        // next none line end string part starts here
        start = curr;
        continue;
      }

      curr++;
    }

    // nothing in there? then the string didn't contain any line end.
    if( cmds.size() == 0 )
    {
      TextCmd* cmd = new AddTextCmd( m, text );
      cmds.push_back(cmd);
    }
    // any chars left? then the last line in the string didn't end with
    // a line end.
    else if( curr > start )
    {
      sc::String part( start, curr-start );
      TextCmd* cmdPart = new AddTextCmd( m, part );
      cmds.push_back(cmdPart);
    }

    if( cmds.size() == 1 )
    {
      return *(cmds.begin());
    }
    else
    {
      return new CompositeTextCmd( cmds );
    }
  }
};

//
///////////////////////////////////////////////////////////////////////////////
//

TextModelImpl::TextModelImpl( const sc::String& sourceName )
{
  M = new Member();
  M->_sourceName = sourceName;
}

TextModelImpl::~TextModelImpl()
{
  delete M;
}

void TextModelImpl::clear()
{
  sc::String name = M->_sourceName;

  delete M;

  M = new Member();
  M->_sourceName = name;
}

const Line& TextModelImpl::getLine( sc::Size lineNr )
{
  return M->getLine( M->getLine( lineNr ) );
}

BlockInfo TextModelImpl::getBlockInfo( int block )
{
  svn::Offset cnt    = 0;
  svn::Offset start  = 0;
  svn::Offset length = 0;
  bool foundStart    = false;

  for( Lines::iterator it = M->_lines.begin(); it != M->_lines.end(); it++, cnt++ )
  {
    if( (*it).getBlockNr() == block )
    {
      if( ! foundStart )
      {
        start      = cnt;
        foundStart = true;
      }
      length++;
    }
  }

  return BlockInfo( start, length );
}

size_t TextModelImpl::getLineCnt()
{
  return M->_lines.size();
}

size_t TextModelImpl::getColumnCnt()
{
  return M->_maxCol; 
}

LineEnd TextModelImpl::getLineEnd()
{
  return M->_lineEnds;
}

unsigned int TextModelImpl::getTabWidth()
{
  return M->_tabWidth;
}

const sc::String& TextModelImpl::getSourceName()
{
  return M->_sourceName;
}

bool TextModelImpl::addLine( const Line& line )
{
  M->checkLineEnd( line );
  M->addLine( line );

  return true;
}

int TextModelImpl::replaceBlock( int block, TextModel* src )
{
  bool foundBlock = false;
  Lines::iterator itStart;
  int cnt    = 0;
  int result = 0;

  for( Lines::iterator it = M->_lines.begin(); it != M->_lines.end(); cnt++ )
  {
    if( (*it).getBlockNr() == block )
    {
      if( ! foundBlock )
      {
        foundBlock = true;
        result     = cnt;
      }

      it      = M->_lines.erase(it);
      itStart = it;
      continue;
    }
    it++;
  }


  BlockInfo bi = src->getBlockInfo(block);

  for( sc::Size j = 0; j < (sc::Size)bi.getLength(); j++ )
  {
    const Line& sl = src->getLine( (sc::Size)bi.getStart() + j );

    if( (! sl.isEmpty()) || ((sl.getType() & ctFlagEmpty) == ctFlagEmpty) )
    {
      Line nl( sl.getLine(), sl.getBlockNr(), (ConflictType)(sl.getType() | ctFlagMerged) );
      M->_lines.insert( itStart, nl );
    }
  }

  M->calcMaxColumn();
  M->rebuildLineNrs();

  // first line of block, for scrolling...
  return result;
}

const Cursor& TextModelImpl::getCursor()
{
  return M->_cursor;
}

const Cursor& TextModelImpl::getCursor2()
{
  return M->_cursor2;
}

void TextModelImpl::setCursor( const Cursor& c )
{
  M->_cursor = c;
}

void TextModelImpl::setCursor2( const Cursor& c )
{
  M->_cursor2 = c;
}

Cursor TextModelImpl::moveCursorRight( bool moveC2 )
{
  return M->moveCursorRight(moveC2);
}

Cursor TextModelImpl::moveCursorLeft( bool moveC2 )
{
  return M->moveCursorLeft(moveC2);
}

Cursor TextModelImpl::moveCursorDown( bool moveC2 )
{
  return M->moveCursorDown(moveC2);
}

Cursor TextModelImpl::moveCursorUp( bool moveC2 )
{
  return M->moveCursorUp(moveC2);
}

int TextModelImpl::getLastColumn()
{
  return M->_lastCol;
}

void TextModelImpl::setLastColumn( int col )
{
  M->_lastCol = col;
}

Cursor TextModelImpl::calcNearestCursorPos( const Cursor& c )
{
  return M->calcNearestCursorPos(c);
}

void TextModelImpl::addText( const sc::String& s )
{
  TextCmd* cmd = TextCmdFactory::createAddTextCmd( M, s );
  M->addCmd( cmd );

  cmd->run();
}

void TextModelImpl::removeTextLeft()
{
  TextCmd* cmd = new RemoveTextLeftCmd( M );
  M->addCmd( cmd );

  cmd->run();
}

void TextModelImpl::removeTextRight()
{
  TextCmd* cmd = new RemoveTextRightCmd( M );
  M->addCmd( cmd );

  cmd->run();
}

sc::String TextModelImpl::getHighlightedText()
{
  CursorPair cp( M->_cursor, M->_cursor2 );
  cp.order();
  Cursor top = cp.getOne();
  Cursor bot = cp.getTwo();

  Tab tab(M->_tabWidth);
  sc::String copy;

  if( top.line() == bot.line() )
  {
    // handle single line, possibly partial line only
    const sc::String& s = getLine( top.line() ).getLine();

    int l = tab.calcCharOffsetForColumn( s.getStr(), top.column() );
    int r = tab.calcCharOffsetForColumn( s.getStr(), bot.column() );

    copy += s.mid( l, r-l );
  }
  else
  {
    // handle first line, possibly partial only
    const sc::String& first = getLine( top.line() ).getLine();
    int l = tab.calcCharOffsetForColumn( first.getStr(), top.column() );
    copy += first.right( first.getCharCnt() - l );

    // handle middle lines, always complete
    for( int i = top.line()+1; i < bot.line(); i++ )
    {
      copy += getLine(i).getLine();
    }

    // handle last line, possibly partial only
    const sc::String& last = getLine( bot.line() ).getLine();
    int r = tab.calcCharOffsetForColumn( last.getStr(), bot.column() );
    copy += last.left( r );
  }

  return copy;
}

void TextModelImpl::undo()
{
  TextCmd* cmd = M->getUndo();

  if( ! cmd )
  {
    return;
  }

  cmd->undo();
}

void TextModelImpl::redo()
{
  TextCmd* cmd = M->getRedo();

  if( ! cmd )
  {
    return;
  }

  cmd->run();
}

bool TextModelImpl::hasUndo()
{
  return M->_cmds.size() > 0;
}

void TextModelImpl::clearUndo()
{
}


syntax highlighted by Code2HTML, v. 0.9.1