/* ====================================================================
* 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