polserver/pol-core/bscript/compiler/file/PrettifyLineBuilder.cpp
turleypol 7627e3d353
"Preview/Initial"- Version sourcecode formatting feature (#626)
* Squash only prettify changes from escript_formating_try

* support of "format-off" / "format-on" comments to mark areas without
formatting

* fix typo

* added FormatterIdentLevel, FormatterMergeEmptyLines cfg entry
better line splits for dicts/arrays

* added several spacing options. the current setting list is now:
//LineWidth
FormatterLineWidth 80
// keep original keyword spelling
FormatterKeepKeywords 0
// number of spaces for ident
FormatterIdentLevel 2
// multiple newlines get merged to a single
FormatterMergeEmptyLines 1
// space between emtpy parenthesis eg foo() vs foo( )
FormatterEmptyParenthesisSpacing 0
// space between emtpy brackets eg struct{} vs struct{ }
FormatterEmptyBracketSpacing 0
// space after/before parenthesis in conditionals
// eg if ( true ) vs if (true)
FormatterConditionalParenthesisSpacing 1
// space after/before parenthesis
// eg foo( true ) vs foo(true)
FormatterParenthesisSpacing 1
// space after/before brackets
// eg array{ true } vs array{true}
FormatterBracketSpacing 1
// add space after delimiter comma or semi in for loops
// eg {1, 2, 3} vs {1,2,3}
FormatterDelimiterSpacing 1
// add space around assignment
// eg a := 1; vs a:=1;
FormatterAssignmentSpacing 1
// add space around comparison
// eg a == 1 vs a==1
FormatterComparisonSpacing 1
// add space around operations
// eg a + 1 vs a+1
FormatterOperatorSpacing 1
// use \r\n as newline instead of \n
FormatterWindowsLineEndings 0

* added tab support
// use tabs instead of spaces
FormatterUseTabs 0
// tab width
FormatterTabWidth 4

* extended example ecompile.cfg with formatter settings

* cleanup

* program args comma is optional...
fixed line comments in group splitting

* since tokenids are now correct no more guessing for linecomments needed

* comments use as start tokenid the whitespace token if its on the same
line

* splitted processing from linebuilding

* renamed TokenPart to FmtToken better matches the purpose.
Comments get directly stored as FmtToken
started with a context enum

* better(?) formatting for groups

* fix eof rawlines
added check that all comments/rawlines got parsed

* const correctness

* datatype adapted to antlr

* fixed warnings

* added InsertNewlineAtEOF

* add newline at the end if the original file had one
dont strip comment whitespace if its somejind of "header block"
use the defined lineending for /* */ comments

* docs

* \r\n is default on windows \n otherwise

* fixed warning

---------

Co-authored-by: Kevin Eady <8634912+KevinEady@users.noreply.github.com>
2024-02-26 21:21:44 +01:00

510 lines
14 KiB
C++

#include "PrettifyLineBuilder.h"
#include "bscript/compilercfg.h"
#include "clib/filecont.h"
#include "clib/logfacility.h"
#include <EscriptGrammar/EscriptLexer.h>
#include <algorithm>
#include <iostream>
#include <utility>
// #define DEBUG_FORMAT_BREAK
namespace Pol::Bscript::Compiler
{
using namespace EscriptGrammar;
const std::vector<std::string>& PrettifyLineBuilder::formattedLines() const
{
return _lines;
}
void PrettifyLineBuilder::setRawLines( std::vector<std::string> rawlines )
{
_rawlines = std::move( rawlines );
}
void PrettifyLineBuilder::setComments( std::vector<FmtToken> comments )
{
_comments = std::move( comments );
}
void PrettifyLineBuilder::setSkipLines( std::vector<Range> skiplines )
{
_skiplines = std::move( skiplines );
}
void PrettifyLineBuilder::addPart( FmtToken part )
{
for ( const auto& skip : _skiplines )
if ( skip.contains( part.pos ) )
return;
_line_parts.emplace_back( std::move( part ) );
}
bool PrettifyLineBuilder::finalize()
{
mergeEOFNonTokens();
return _line_parts.empty() && _comments.empty() && _skiplines.empty();
}
const std::vector<FmtToken>& PrettifyLineBuilder::currentTokens() const
{
return _line_parts;
}
void PrettifyLineBuilder::mergeRawContent( size_t nextlineno )
{
for ( auto itr = _skiplines.begin(); itr != _skiplines.end(); )
{
if ( itr->start.line_number < nextlineno )
{
mergeCommentsBefore( itr->start.line_number );
addEmptyLines( itr->start.line_number );
for ( size_t i = itr->start.line_number - 1; i < itr->end.line_number && i < _rawlines.size();
++i )
{
_lines.emplace_back( _rawlines[i] );
_last_line = i + 1;
}
itr = _skiplines.erase( itr );
continue;
}
++itr;
}
}
void PrettifyLineBuilder::mergeCommentsBefore( size_t nextlineno )
{
// add comments before current line
while ( !_comments.empty() )
{
if ( _comments.front().pos.line_number < nextlineno )
{
addEmptyLines( _comments.front().pos.line_number );
_lines.push_back( identSpacing() + _comments.front().text );
_last_line = _comments.front().pos_end.line_number;
_comments.erase( _comments.begin() );
}
else
break;
}
}
void PrettifyLineBuilder::mergeComments()
{
if ( _line_parts.empty() )
return;
mergeRawContent( _line_parts.front().pos.line_number );
mergeCommentsBefore( _line_parts.front().pos.line_number );
// add comments inbetween
for ( size_t i = 0; i < _line_parts.size(); )
{
if ( _comments.empty() )
break;
if ( _line_parts[i].pos.token_index > _comments.front().pos.token_index )
{
auto info = _comments.front();
info.group = _currentgroup;
_line_parts.insert( _line_parts.begin() + i, std::move( info ) );
_comments.erase( _comments.begin() );
}
else
++i;
}
// add comments at the end of the current line
if ( !_comments.empty() )
{
if ( _comments.front().pos.line_number == _line_parts.back().pos.line_number &&
_comments.front().pos.token_index <= _line_parts.back().pos.token_index + 1 )
{
addPart( _comments.front() );
_comments.erase( _comments.begin() );
}
}
}
void PrettifyLineBuilder::buildLine( size_t current_ident )
{
_currident = current_ident;
if ( _line_parts.empty() )
return;
auto stripline = []( std::string& line )
{
if ( !line.empty() )
{
auto lastchar = line.find_last_not_of( ' ' );
line.erase( line.begin() + lastchar + 1, line.end() );
}
};
mergeComments();
// fill lines with final strings splitted at breakpoints
// <splitted string, forcenewline, groupid>
std::vector<std::tuple<std::string, bool, size_t, size_t>> lines;
{
std::string line;
size_t firstgroup = 0;
for ( size_t i = 0; i < _line_parts.size(); ++i )
{
if ( line.empty() )
firstgroup = _line_parts[i].group;
line += _line_parts[i].text;
// add space if set, but not if the following part is attached
if ( _line_parts[i].style & FmtToken::SPACE )
{
if ( i + 1 < _line_parts.size() )
{
if ( !( _line_parts[i + 1].style & FmtToken::ATTACHED ) )
line += ' ';
}
else
line += ' ';
}
if ( _line_parts[i].style & FmtToken::FORCED_BREAK )
{
lines.emplace_back(
std::make_tuple( std::move( line ), true, _line_parts[i].group, firstgroup ) );
line.clear();
}
// start a new line if breakpoint
else if ( _line_parts[i].style & FmtToken::BREAKPOINT )
{
// if the next part is attached, dont break now and instead later
// eg dont split blubb[1]()
bool skip = false;
if ( i + 1 < _line_parts.size() )
{
// next one is attached
if ( _line_parts[i + 1].style & FmtToken::ATTACHED )
{
// search for space or breakpoint
for ( size_t j = i + 1; j < _line_parts.size(); ++j )
{
if ( _line_parts[j].style & FmtToken::ATTACHED )
skip = true;
if ( _line_parts[j].style & FmtToken::SPACE ||
_line_parts[j].style & FmtToken::BREAKPOINT )
{
_line_parts[j].style |= FmtToken::BREAKPOINT;
skip = true;
break;
}
}
}
}
if ( skip )
continue;
lines.emplace_back(
std::make_tuple( std::move( line ), false, _line_parts[i].group, firstgroup ) );
line.clear();
}
}
// add remainings
if ( !line.empty() )
{
lines.emplace_back( std::make_tuple( std::move( line ), false, _line_parts.back().group,
_line_parts.back().group ) );
line.clear();
}
}
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "BREAK " );
for ( auto& [l, forced, group, firstgroup] : lines )
INFO_PRINTLN( "\"{}\" {}-{}", l, group, firstgroup );
#endif
// add newline from original sourcecode
addEmptyLines( _line_parts.front().pos.line_number );
// sum up linelength, are groups inside
bool groups = false;
size_t linelength = 0;
for ( auto& [l, forced, group, firstgroup] : lines )
{
if ( group != 0 )
groups = true;
linelength += l.size();
}
std::string line;
// split based on groups
if ( groups && linelength > compilercfg.FormatterLineWidth )
{
// store for each group thr alignment
std::map<size_t, size_t> alignmentspace = {};
size_t lastgroup = 0xffffFFFF;
bool newline = false;
auto ident = identSpacing();
line = ident;
bool groupdiffered = false;
for ( auto& [l, forced, group, firstgroup] : lines )
{
if ( forced )
{
line += l;
stripline( line );
_lines.emplace_back( std::move( line ) );
line = ident;
continue;
}
if ( lastgroup == 0xffffFFFF )
{
line += l; // first part
alignmentspace[firstgroup] = line.size() - ident.size();
alignmentspace[group] = line.size() - ident.size();
newline = true;
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "first {}", line );
#endif
}
else if ( lastgroup < firstgroup ) // new group
{
groupdiffered = true;
bool newgroup = false;
if ( !alignmentspace.count( firstgroup ) )
{
newgroup = true;
alignmentspace[firstgroup] = line.size() - ident.size();
}
// if its not a new group and not a fresh line start a new one
// nested structs eg should start at the same line
if ( !newgroup && !newline )
{
stripline( line );
if ( !line.empty() )
_lines.emplace_back( std::move( line ) );
line = alignmentSpacing( alignmentspace[firstgroup] );
line += ident;
line += l;
newline = true;
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "!new {} - {}", line, alignmentspace );
#endif
}
else
{
line += l;
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "new {} - {}", line, alignmentspace );
#endif
}
if ( !alignmentspace.count( group ) )
alignmentspace[group] = line.size() - ident.size();
}
else if ( lastgroup > firstgroup ) // descending
{
newline = true;
stripline( line );
if ( !line.empty() )
_lines.emplace_back( std::move( line ) );
line = alignmentSpacing( alignmentspace[firstgroup] );
line += ident;
line += l;
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "desc {} - {}", line, alignmentspace );
#endif
}
else // same group
{
// only if we already had a different group split the line
if ( groupdiffered )
{
newline = false;
stripline( line );
if ( !line.empty() )
_lines.emplace_back( std::move( line ) );
line = alignmentSpacing( alignmentspace[firstgroup] );
line += ident;
}
line += l;
#ifdef DEBUG_FORMAT_BREAK
INFO_PRINTLN( "same {} - {}", line, alignmentspace );
#endif
if ( line.size() > compilercfg.FormatterLineWidth )
{
stripline( line );
if ( !line.empty() )
_lines.emplace_back( std::move( line ) );
line = alignmentSpacing( alignmentspace[firstgroup] );
line += ident;
}
}
lastgroup = firstgroup;
}
}
else // split based on parts
{
// build final lines
size_t alignmentspace = 0;
for ( auto& [l, forced, group, firstgroup] : lines )
{
// following lines need to be aligned
if ( line.empty() && alignmentspace )
line = alignmentSpacing( alignmentspace );
// first breakpoint defines the alignment and add initial ident level
if ( !alignmentspace )
{
auto ident = identSpacing();
alignmentspace = l.size() + ident.size();
line += ident;
}
line += l;
// linewidth reached add current line, start a new one
if ( line.size() > compilercfg.FormatterLineWidth || forced )
{
stripline( line );
_lines.emplace_back( std::move( line ) );
line.clear();
}
}
}
if ( !line.empty() )
{
stripline( line );
_lines.emplace_back( std::move( line ) );
}
_last_line = _line_parts.back().pos.line_number;
_line_parts.clear();
}
void PrettifyLineBuilder::addEmptyLines( size_t line_number )
{
if ( compilercfg.FormatterMergeEmptyLines )
{
if ( line_number > _last_line + 1 )
_lines.emplace_back( "" );
}
else
{
while ( line_number > _last_line + 1 )
{
_lines.emplace_back( "" );
++_last_line;
}
}
}
int PrettifyLineBuilder::closingParenthesisStyle( size_t begin_size )
{
auto style = FmtToken::SPACE | FmtToken::BREAKPOINT;
if ( _line_parts.size() == begin_size )
{
if ( !compilercfg.FormatterEmptyParenthesisSpacing )
style |= FmtToken::ATTACHED;
else if ( !_line_parts.empty() )
_line_parts.back().style |= FmtToken::SPACE;
}
else if ( !compilercfg.FormatterParenthesisSpacing )
style |= FmtToken::ATTACHED;
return style;
}
int PrettifyLineBuilder::closingBracketStyle( size_t begin_size )
{
auto style = FmtToken::SPACE | FmtToken::BREAKPOINT;
if ( _line_parts.size() == begin_size )
{
if ( !compilercfg.FormatterEmptyBracketSpacing )
style |= FmtToken::ATTACHED;
else if ( !_line_parts.empty() )
_line_parts.back().style |= FmtToken::SPACE;
}
else if ( !compilercfg.FormatterBracketSpacing )
style |= FmtToken::ATTACHED;
return style;
}
int PrettifyLineBuilder::openingParenthesisStyle()
{
return FmtToken::ATTACHED | FmtToken::BREAKPOINT |
( compilercfg.FormatterParenthesisSpacing ? FmtToken::SPACE : FmtToken::NONE );
}
int PrettifyLineBuilder::openingBracketStyle()
{
return FmtToken::ATTACHED | FmtToken::BREAKPOINT |
( compilercfg.FormatterBracketSpacing ? FmtToken::SPACE : FmtToken::NONE );
}
int PrettifyLineBuilder::delimiterStyle()
{
return FmtToken::ATTACHED | FmtToken::BREAKPOINT |
( compilercfg.FormatterDelimiterSpacing ? FmtToken::SPACE : FmtToken::NONE );
}
int PrettifyLineBuilder::terminatorStyle()
{
return FmtToken::SPACE | FmtToken::ATTACHED | FmtToken::BREAKPOINT;
}
int PrettifyLineBuilder::assignmentStyle()
{
if ( !compilercfg.FormatterAssignmentSpacing )
return FmtToken::ATTACHED;
return FmtToken::SPACE;
}
int PrettifyLineBuilder::comparisonStyle()
{
if ( !compilercfg.FormatterComparisonSpacing )
return FmtToken::ATTACHED;
return FmtToken::SPACE;
}
int PrettifyLineBuilder::operatorStyle()
{
if ( !compilercfg.FormatterOperatorSpacing )
return FmtToken::ATTACHED;
return FmtToken::SPACE;
}
std::string PrettifyLineBuilder::identSpacing()
{
if ( !compilercfg.FormatterUseTabs )
return std::string( _currident * compilercfg.FormatterIdentLevel, ' ' );
size_t total = _currident * compilercfg.FormatterIdentLevel;
size_t tabs = total / compilercfg.FormatterTabWidth;
size_t remaining = total % compilercfg.FormatterTabWidth;
return std::string( tabs, '\t' ) + std::string( remaining, ' ' );
}
std::string PrettifyLineBuilder::alignmentSpacing( size_t count )
{
if ( !count )
return {};
if ( !compilercfg.FormatterUseTabs )
return std::string( count, ' ' );
size_t tabs = count / compilercfg.FormatterTabWidth;
size_t remaining = count % compilercfg.FormatterTabWidth;
return std::string( tabs, '\t' ) + std::string( remaining, ' ' );
}
void PrettifyLineBuilder::mergeEOFNonTokens()
{
mergeRawContent( _last_line );
while ( !_comments.empty() )
{
addEmptyLines( _comments.front().pos.line_number );
_lines.push_back( identSpacing() + _comments.front().text );
_last_line = _comments.front().pos_end.line_number;
mergeRawContent( _last_line );
_comments.erase( _comments.begin() );
}
// purge raw content
mergeRawContent( std::numeric_limits<size_t>::max() );
// if the original file ends with a newline, make sure to also end
if ( !_lines.empty() && !_lines.back().empty() && !_rawlines.empty() && _rawlines.back().empty() )
_lines.push_back( "" );
}
} // namespace Pol::Bscript::Compiler
fmt::format_context::iterator fmt::formatter<Pol::Bscript::Compiler::FmtToken>::format(
const Pol::Bscript::Compiler::FmtToken& t, fmt::format_context& ctx ) const
{
return fmt::formatter<std::string>::format(
fmt::format( "{} ({}:{}:{})", t.text, t.style, t.pos.line_number, t.pos.token_index ), ctx );
}