/* ==================================================================== * 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 #include #include #include #include #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() { }