#include #include #include #include namespace EE { namespace UI { namespace Doc { const char NON_WORD_CHARS[] = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"; bool TextDocument::isNonWord( String::StringBaseType ch ) { for ( size_t i = 0; i < eeARRAY_SIZE( NON_WORD_CHARS ); i++ ) { if ( static_cast( NON_WORD_CHARS[i] ) == ch ) { return true; } } return false; } TextDocument::TextDocument() : mUndoStack( this ) {} void TextDocument::reset() { mFilename = "unsaved"; mSelection.set( {0, 0}, {0, 0} ); mLines.clear(); notifyTextChanged(); notifyCursorChanged(); notifySelectionChanged(); } void TextDocument::loadFromPath( const std::string& path ) { reset(); mFilename = path; std::string line; std::ifstream file( path ); while ( std::getline( file, line ) ) { std::istringstream iss( line ); if ( mLines.empty() && line.size() >= 3 ) { // Check UTF-8 BOM header if ( (char)0xef == line[0] && (char)0xbb == line[1] && (char)0xbf == line[2] ) { line = line.substr( 3 ); } } // Check CLRF if ( !line.empty() && line[line.size() - 1] == '\r' ) { line = line.substr( 0, line.size() - 1 ); mIsCLRF = true; } mLines.emplace_back( String( line + "\n" ) ); } if ( mLines.empty() ) { mLines.emplace_back( String( "\n" ) ); } else if ( mLines[mLines.size() - 1].at( mLines[mLines.size() - 1].size() - 1 ) == '\n' ) { mLines.emplace_back( String( "\n" ) ); } } void TextDocument::save( const std::string& ) {} const std::string TextDocument::getFilename() const { return mFilename; } void TextDocument::setSelection( TextPosition position ) { setSelection( position, position ); } void TextDocument::setSelection( TextPosition start, TextPosition end, bool swap ) { if ( ( start == mSelection.start() && end == mSelection.end() && !swap ) || ( start == mSelection.end() && end == mSelection.start() && swap ) ) return; if ( swap ) { auto posT = start; start = end; end = posT; } if ( start == end ) { start = end = sanitizePosition( start ); } else { start = sanitizePosition( start ); end = sanitizePosition( end ); } if ( mSelection != TextRange( start, end ) ) { mSelection.set( start, end ); notifyCursorChanged(); notifySelectionChanged(); } } void TextDocument::setSelection( TextRange range ) { setSelection( range.start(), range.end() ); } TextRange TextDocument::getSelection( bool sort ) const { return sort ? mSelection.normalized() : mSelection; } const TextRange& TextDocument::getSelection() const { return mSelection; } String& TextDocument::line( const size_t& index ) { return mLines[index]; } const String& TextDocument::line( const size_t& index ) const { return mLines[index]; } size_t TextDocument::lineCount() const { return mLines.size(); } std::vector& TextDocument::lines() { return mLines; } bool TextDocument::hasSelection() const { return mSelection.start() != mSelection.end(); } String TextDocument::getText( const TextRange& range ) const { TextRange nrange = range.normalized(); if ( nrange.start().line() == nrange.end().line() ) { return mLines[nrange.start().line()].substr( nrange.start().column(), nrange.end().column() - nrange.start().column() ); } std::vector lines = {mLines[nrange.start().line()].substr( nrange.start().column() )}; for ( auto i = nrange.start().line() + 1; i <= nrange.end().line() - 1; i++ ) { lines.emplace_back( mLines[i] ); } lines.emplace_back( mLines[nrange.end().line()].substr( 0, nrange.end().column() ) ); return String::join( lines, -1 ); } String TextDocument::getSelectedText() const { return getText( getSelection() ); } String::StringBaseType TextDocument::getChar( const TextPosition& position ) const { auto pos = sanitizePosition( position ); return mLines[pos.line()][pos.column()]; } TextPosition TextDocument::insert( const TextPosition& position, const String& text ) { mUndoStack.clearRedoStack(); return insert( position, text, mUndoStack.getUndoStackContainer(), mTimer.getElapsedTime() ); } TextPosition TextDocument::insert( const TextPosition& position, const String& text, UndoStackContainer& undoStack, const Time& time ) { TextPosition cursor = position; for ( size_t i = 0; i < text.length(); ++i ) { cursor = insert( cursor, text[i] ); } mUndoStack.pushSelection( undoStack, getSelection(), time ); mUndoStack.pushRemove( undoStack, {position, cursor}, time ); notifyTextChanged(); return cursor; } TextPosition TextDocument::insert( TextPosition position, const String::StringBaseType& ch ) { position = sanitizePosition( position ); bool atHead = position.column() == 0; bool atTail = position.column() == (Int64)line( position.line() ).length() - 1; if ( ch == '\n' ) { if ( atTail || atHead ) { size_t row = position.line(); String line_content; for ( size_t i = position.column(); i < line( row ).length(); i++ ) line_content.append( line( row )[i] ); mLines.insert( mLines.begin() + position.line() + ( atTail ? 1 : 0 ), String( "\n" ) ); return atTail ? TextPosition( position.line() + 1, line( position.line() + 1 ).length() ) : TextPosition( position.line() + 1, 0 ); } String newLine = line( position.line() ) .substr( position.column(), line( position.line() ).length() - position.column() ); line( position.line() ) = line( position.line() ).substr( 0, position.column() ); if ( newLine.empty() ) { eePRINTL( "wtf" ); } mLines.insert( mLines.begin() + position.line() + 1, std::move( newLine ) ); return {position.line() + 1, 0}; } line( position.line() ).insert( line( position.line() ).begin() + position.column(), ch ); return {position.line(), position.column() + 1}; } void TextDocument::remove( TextPosition position ) { remove( TextRange( position, position ) ); } void TextDocument::remove( TextRange range ) { mUndoStack.clearRedoStack(); range = range.normalized(); range.setStart( sanitizePosition( range.start() ) ); range.setEnd( sanitizePosition( range.end() ) ); remove( range, mUndoStack.getUndoStackContainer(), mTimer.getElapsedTime() ); } void TextDocument::remove( TextRange range, UndoStackContainer& undoStack, const Time& time ) { if ( !range.isValid() ) return; mUndoStack.pushSelection( undoStack, getSelection(), time ); mUndoStack.pushInsert( undoStack, getText( range ), range.start(), time ); // First delete all the lines in between the first and last one. for ( auto i = range.start().line() + 1; i < range.end().line(); ) { mLines.erase( mLines.begin() + i ); range.end().setLine( range.end().line() - 1 ); } if ( range.start().line() == range.end().line() ) { // Delete within same line. auto& line = this->line( range.start().line() ); bool wholeLineIsSelected = range.start().column() == 0 && range.end().column() == (Int64)line.length(); if ( wholeLineIsSelected ) { line.clear(); } else { auto beforeSelection = line.substr( 0, range.start().column() ); auto afterSelection = line.substr( range.end().column(), line.length() - range.end().column() ); line.assign( beforeSelection + afterSelection ); } } else { // Delete across a newline, merging lines. eeASSERT( range.start().line() == range.end().line() - 1 ); auto& firstLine = line( range.start().line() ); auto& secondLine = line( range.end().line() ); auto beforeSelection = firstLine.substr( 0, range.start().column() ); auto afterSelection = secondLine.substr( range.end().column(), secondLine.length() - range.end().column() ); if ( beforeSelection.empty() && afterSelection.empty() ) { beforeSelection += '\n'; } firstLine.assign( beforeSelection + afterSelection ); mLines.erase( mLines.begin() + range.end().line() ); } if ( lines().empty() ) { mLines.emplace_back( String( "\n" ) ); } notifyTextChanged(); } TextPosition TextDocument::positionOffset( TextPosition position, int columnOffset ) const { position = sanitizePosition( position ); position.setColumn( position.column() + columnOffset ); while ( position.line() > 0 && position.column() < 0 ) { position.setLine( position.line() - 1 ); position.setColumn( eemax( 0, position.column() + mLines[position.line()].size() ) ); } while ( position.line() < (Int64)mLines.size() && position.column() > (Int64)eemax( 0, mLines[position.line()].size() - 1 ) ) { position.setColumn( position.column() - mLines[position.line()].size() - 1 ); position.setLine( position.line() + 1 ); } return sanitizePosition( position ); } TextPosition TextDocument::positionOffset( TextPosition position, TextPosition offset ) const { return sanitizePosition( position + offset ); } TextPosition TextDocument::nextChar( TextPosition position ) const { return positionOffset( position, TextPosition( 0, 1 ) ); } TextPosition TextDocument::previousChar( TextPosition position ) const { return positionOffset( position, TextPosition( 0, -1 ) ); } TextPosition TextDocument::previousWordBoundary( TextPosition position ) const { auto ch = getChar( positionOffset( position, -1 ) ); bool inWord = !isNonWord( ch ); String::StringBaseType nextChar = 0; do { TextPosition curPos = position; position = positionOffset( position, -1 ); if ( curPos == position ) { break; } nextChar = getChar( positionOffset( position, -1 ) ); } while ( ( inWord && !isNonWord( nextChar ) ) || ( !inWord && nextChar == ch ) ); return position; } TextPosition TextDocument::nextWordBoundary( TextPosition position ) const { auto ch = getChar( position ); bool inWord = !isNonWord( ch ); String::StringBaseType nextChar = 0; do { TextPosition curPos = position; position = positionOffset( position, 1 ); if ( curPos == position ) { break; } nextChar = getChar( position ); } while ( ( inWord && !isNonWord( nextChar ) ) || ( !inWord && nextChar == ch ) ); return position; } TextPosition TextDocument::startOfWord( TextPosition position ) const { while ( true ) { TextPosition curPos = positionOffset( position, -1 ); String::StringBaseType ch = getChar( curPos ); if ( isNonWord( ch ) or position == curPos ) { break; } position = curPos; } return position; } TextPosition TextDocument::endOfWord( TextPosition position ) const { while ( true ) { TextPosition curPos = positionOffset( position, 1 ); String::StringBaseType ch = getChar( position ); if ( isNonWord( ch ) or position == curPos ) { break; } position = curPos; } return position; } TextPosition TextDocument::startOfLine( TextPosition position ) const { position = sanitizePosition( position ); return TextPosition( position.line(), 0 ); } TextPosition TextDocument::endOfLine( TextPosition position ) const { position = sanitizePosition( position ); return TextPosition( position.line(), mLines[position.line()].size() - 1 ); } TextPosition TextDocument::startOfDoc() const { return TextPosition( 0, 0 ); } TextPosition TextDocument::endOfDoc() const { return TextPosition( mLines.size() - 1, mLines[mLines.size() - 1].size() - 1 ); } TextPosition TextDocument::getAbsolutePosition( TextPosition position ) const { position = sanitizePosition( position ); const String& string = line( position.line() ); size_t tabCount = string.substr( 0, position.column() ).countChar( '\t' ); return TextPosition( position.line(), position.column() - tabCount + tabCount * getTabWidth() ); } Int64 TextDocument::getRelativeColumnOffset( TextPosition position ) const { const String& line = mLines[position.line()]; Int64 length = eemin( position.column(), line.size() - 1 ); Int64 offset = 0; for ( Int64 i = 0; i <= length; ++i ) { if ( offset >= position.column() ) { return i; } if ( line[i] == '\t' ) { offset += getTabWidth(); } else if ( line[i] != '\n' && line[i] != '\r' ) { offset += 1; } } return length; } void TextDocument::deleteTo( int offset ) { TextPosition cursorPos = getSelection( true ).start(); if ( hasSelection() ) { remove( getSelection() ); } else { TextPosition delPos = positionOffset( cursorPos, offset ); TextRange range( cursorPos, delPos ); remove( range ); range = range.normalized(); cursorPos = range.start(); } setSelection( cursorPos ); } void TextDocument::deleteSelection() { TextPosition cursorPos = getSelection( true ).start(); remove( getSelection() ); setSelection( cursorPos ); } void TextDocument::selectTo( TextPosition position ) { setSelection( TextRange( sanitizePosition( position ), getSelection().end() ) ); } void TextDocument::selectTo( int offset ) { const TextRange& range = getSelection(); TextPosition posOffset = positionOffset( range.start(), offset ); setSelection( TextRange( posOffset, range.end() ) ); } void TextDocument::moveTo( TextPosition offset ) { setSelection( getSelection().start() + offset ); } void TextDocument::moveTo( int columnOffset ) { setSelection( positionOffset( getSelection().start(), columnOffset ) ); } void TextDocument::textInput( const String& text ) { if ( hasSelection() ) { deleteTo( 0 ); } setSelection( insert( getSelection().start(), text ) ); } void TextDocument::registerClient( TextDocument::Client& client ) { mClients.insert( &client ); } void TextDocument::unregisterClient( TextDocument::Client& client ) { mClients.erase( &client ); } void TextDocument::moveToPreviousChar() { if ( hasSelection() ) { TextRange selection = getSelection( true ); setSelection( selection.end() ); } else { setSelection( positionOffset( getSelection().start(), -1 ) ); } } void TextDocument::moveToNextChar() { if ( hasSelection() ) { TextRange selection = getSelection( true ); setSelection( selection.start() ); } else { setSelection( positionOffset( getSelection().start(), 1 ) ); } } void TextDocument::moveToPreviousWord() { if ( hasSelection() ) { TextRange selection = getSelection( true ); setSelection( selection.end() ); } else { setSelection( previousWordBoundary( getSelection().start() ) ); } } void TextDocument::moveToNextWord() { if ( hasSelection() ) { TextRange selection = getSelection( true ); setSelection( selection.start() ); } else { setSelection( nextWordBoundary( getSelection().start() ) ); } } void TextDocument::moveToPreviousLine( Int64 lastColIndex ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() - 1 ); if ( pos.line() >= 0 ) { lastColIndex = getRelativeColumnOffset( TextPosition( pos.line(), lastColIndex ) ); } pos.setColumn( lastColIndex ); setSelection( pos ); } void TextDocument::moveToNextLine( Int64 lastColIndex ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() + 1 ); if ( pos.line() < (Int64)mLines.size() ) { lastColIndex = getRelativeColumnOffset( TextPosition( pos.line(), lastColIndex ) ); } pos.setColumn( lastColIndex ); setSelection( pos ); } void TextDocument::moveToPreviousPage( Int64 pageSize ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() - pageSize ); setSelection( pos ); } void TextDocument::moveToNextPage( Int64 pageSize ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() + pageSize ); setSelection( pos ); } void TextDocument::deleteToPreviousChar() { deleteTo( -1 ); } void TextDocument::deleteToNextChar() { deleteTo( 1 ); } void TextDocument::deleteToPreviousWord() { deleteTo( previousWordBoundary( getSelection().start() ) ); } void TextDocument::deleteToNextWord() { deleteTo( nextWordBoundary( getSelection().start() ) ); } void TextDocument::selectToPreviousChar() { selectTo( -1 ); } void TextDocument::selectToNextChar() { selectTo( 1 ); } void TextDocument::selectToPreviousWord() { setSelection( {previousWordBoundary( getSelection().start() ), getSelection().end()} ); } void TextDocument::selectToNextWord() { setSelection( {nextWordBoundary( getSelection().start() ), getSelection().end()} ); } void TextDocument::selectWord() { setSelection( {nextWordBoundary( getSelection().start() ), previousWordBoundary( getSelection().start() )} ); } void TextDocument::selectToPreviousLine( Int64 lastColIndex ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() - 1 ); if ( pos.line() >= 0 ) { lastColIndex = getRelativeColumnOffset( TextPosition( pos.line(), lastColIndex ) ); } pos.setColumn( lastColIndex ); setSelection( TextRange( pos, getSelection().end() ) ); } void TextDocument::selectToNextLine( Int64 lastColIndex ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() + 1 ); if ( pos.line() < (Int64)mLines.size() ) { lastColIndex = getRelativeColumnOffset( TextPosition( pos.line(), lastColIndex ) ); } pos.setColumn( lastColIndex ); setSelection( TextRange( pos, getSelection().end() ) ); } void TextDocument::selectToStartOfLine() { selectTo( startOfLine( getSelection().start() ) ); } void TextDocument::selectToEndOfLine() { selectTo( endOfLine( getSelection().start() ) ); } void TextDocument::selectToPreviousPage( Int64 pageSize ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() - pageSize ); selectTo( pos ); } void TextDocument::selectToNextPage( Int64 pageSize ) { TextPosition pos = getSelection().start(); pos.setLine( pos.line() + pageSize ); selectTo( pos ); } void TextDocument::selectAll() { setSelection( startOfDoc(), endOfDoc() ); } void TextDocument::newLine() { String input( "\n" ); TextPosition start = getSelection().start(); if ( start.line() >= 0 && start.line() < (Int64)mLines.size() ) { String& ln = line( start.line() ); size_t to = eemin( ln.size(), start.column() ); int indent = 0; for ( size_t i = 0; i < to; i++ ) { if ( '\t' == ln[i] || ' ' == ln[i] ) { indent++; } else { break; } } if ( indent ) { input.append( ln.substr( 0, indent ) ); } } textInput( input ); } void TextDocument::insertAtStartOfSelectedLines( String text, bool skipEmpty ) { TextPosition prevStart = getSelection().start(); TextRange range = getSelection( true ); bool swap = prevStart != range.start(); for ( auto i = range.start().line(); i <= range.end().line(); i++ ) { const String& line = this->line( i ); if ( !skipEmpty || line.length() != 1 ) { insert( {i, 0}, text ); } } setSelection( TextPosition( range.start().line(), range.start().column() + text.size() ), TextPosition( range.end().line(), range.end().column() + text.size() ), swap ); } void TextDocument::removeFromStartOfSelectedLines( String text, bool skipEmpty ) { TextPosition prevStart = getSelection().start(); TextRange range = getSelection( true ); bool swap = prevStart != range.start(); for ( auto i = range.start().line(); i <= range.end().line(); i++ ) { const String& line = this->line( i ); if ( !skipEmpty || line.length() != 1 ) { if ( line.substr( 0, text.length() ) == text ) { remove( {{i, 0}, {i, static_cast( text.length() )}} ); } } } setSelection( TextPosition( range.start().line(), range.start().column() - text.size() ), TextPosition( range.end().line(), range.end().column() - text.size() ), swap ); } void TextDocument::indent() { if ( hasSelection() ) { insertAtStartOfSelectedLines( getIndentString(), false ); } else { textInput( getIndentString() ); } } void TextDocument::unindent() { removeFromStartOfSelectedLines( getIndentString(), false ); } String TextDocument::getIndentString() { if ( IndentSpaces == mIndentType ) { return String( std::string( mTabWidth, ' ' ) ); } return String( "\t" ); } const Uint32& TextDocument::getTabWidth() const { return mTabWidth; } void TextDocument::setTabWidth( const Uint32& tabWidth ) { mTabWidth = tabWidth; } void TextDocument::deleteTo( TextPosition position ) { TextPosition cursorPos = getSelection( true ).start(); if ( hasSelection() ) { remove( getSelection() ); } else { TextRange range( cursorPos, position ); remove( range ); range = range.normalized(); cursorPos = range.start(); } setSelection( cursorPos ); } void TextDocument::print() const { for ( size_t i = 0; i < mLines.size(); i++ ) printf( "%s", mLines[i].toUtf8().c_str() ); } TextPosition TextDocument::sanitizePosition( const TextPosition& position ) const { Int64 line = eeclamp( position.line(), 0UL, mLines.size() - 1 ); Int64 col = eeclamp( position.column(), 0UL, eemax( 0, mLines[line].size() - 1 ) ); return {line, col}; } const TextDocument::IndentType& TextDocument::getIndentType() const { return mIndentType; } void TextDocument::setIndentType( const IndentType& indentType ) { mIndentType = indentType; } void TextDocument::undo() { mUndoStack.undo(); } void TextDocument::redo() { mUndoStack.redo(); } void TextDocument::notifyTextChanged() { for ( size_t i = 0; i < mLines.size(); i++ ) { if ( mLines[i].empty() ) { eePRINTL( "wtf" ); } } for ( auto& client : mClients ) { client->onDocumentTextChanged(); } } void TextDocument::notifyCursorChanged() { for ( auto& client : mClients ) { client->onDocumentCursorChange( getSelection().start() ); } } void TextDocument::notifySelectionChanged() { for ( auto& client : mClients ) { client->onDocumentSelectionChange( getSelection() ); } } TextDocument::Client::~Client() {} }}} // namespace EE::UI::Doc