Auto-Close brackets improvements:

1. Word boundary behavior: Typing an opening bracket directly preceding an alphanumeric character prevents auto-closing. (e.g. typing ( before word correctly inserts ( without closing it).
2. Whitespace boundary behavior: Typing an opening bracket before a space triggers normal auto-closing.
3. Forward unbalanced brackets: Typing an opening bracket when there's an unmatched closing bracket ahead on the same line prevents auto-closing (so typing ( when the line is () ) doesn't insert an extra )).
4. Balanced brackets: Regular auto-closing when brackets are completely matched.
5. Quote balancing: If the line already has balanced quotes and your cursor is right before a quote, typing a quote will just overwrite/step over the right quote instead of adding a new pair.

(SpartanJ/ecode#888)
This commit is contained in:
Martín Lucas Golini
2026-04-23 16:12:25 -03:00
parent 8fef89138b
commit 0bb3f1157f
2 changed files with 104 additions and 20 deletions

View File

@@ -2214,27 +2214,67 @@ std::vector<bool> TextDocument::autoCloseBrackets( const String& text ) {
continue;
}
if ( isClose && !isSame )
if ( isClose && !isSame ) {
mustClose = false;
} else if ( !isClose && !isNonWord( ch ) ) {
mustClose = false;
}
}
if ( mustClose && isSame ) {
Int64 left = sel.start().column() - 1;
Int64 right = sel.start().column();
const String& lineText = line( sel.start().line() ).getText();
Int64 len = lineText.size();
Int64 limitLeft = eemax<Int64>( 0ll, sel.start().column() - 512 );
Int64 limitRight = eemin<Int64>( len, sel.start().column() + 512 );
int unclosedQuotes = 0;
while ( left >= limitLeft || right < limitRight ) {
bool matchLeft = left >= limitLeft && lineText[left] == text[0];
bool matchRight = right < limitRight && lineText[right] == text[0];
if ( matchLeft && matchRight ) {
left--;
right++;
} else if ( matchLeft ) {
unclosedQuotes++;
left--;
} else if ( matchRight ) {
unclosedQuotes++;
right++;
} else {
if ( left >= limitLeft )
left--;
if ( right < limitRight )
right++;
}
}
if ( unclosedQuotes % 2 != 0 )
mustClose = false;
}
if ( mustClose && !isSame && !isClose ) {
int balance = 0;
int unmatchedRight = 0;
const String& lineText = line( sel.start().line() ).getText();
Int64 len = lineText.size();
Int64 limitLeft = eemax<Int64>( 0, sel.start().column() - 512 );
Int64 limitRight = eemin<Int64>( len, sel.start().column() + 512 );
for ( Int64 k = limitLeft; k < limitRight; ++k ) {
if ( lineText[k] == text[0] ) {
balance++;
} else if ( lineText[k] == closeChar ) {
if ( balance > 0 ) {
balance--;
} else if ( k >= sel.start().column() ) {
unmatchedRight++;
}
}
}
if ( unmatchedRight > 0 )
mustClose = false;
}
if ( mustClose ) {
/* // I'm not entirely convinced about this
TextPosition openStart = positionOffset( sel.start(), 1 );
if ( openStart != sel.start() ) {
int maxIt = 100;
while ( maxIt-- > 0 && openStart < endOfDoc() &&
isSpace( getChar( openStart ) ) ) {
openStart = nextChar( openStart );
}
if ( openStart < endOfDoc() && maxIt > 0 &&
getChar( openStart ) == closeChar ) {
inserted.push_back( false );
continue;
}
}
*/
setSelection(
i, positionOffset( insert( i, sel.start(), text + String( closeChar ) ), -1 ) );
inserted.push_back( true );

View File

@@ -113,9 +113,9 @@ UTEST( TextDocument, newLineMultiCursorAutoIndent ) {
doc.insert( 0, { 2, 3 }, ")" );
// Cursors between all pairs
doc.resetSelection( TextRanges( std::vector<TextRange>{
TextRange( { 0, 1 }, { 0, 1 } ), TextRange( { 1, 2 }, { 1, 2 } ),
TextRange( { 2, 3 }, { 2, 3 } ) } ) );
doc.resetSelection( TextRanges( std::vector<TextRange>{ TextRange( { 0, 1 }, { 0, 1 } ),
TextRange( { 1, 2 }, { 1, 2 } ),
TextRange( { 2, 3 }, { 2, 3 } ) } ) );
doc.newLine();
@@ -145,4 +145,48 @@ UTEST( TextDocument, newLineNormal ) {
EXPECT_STDSTREQ( TextPosition( 1, 2 ).toString(), doc.getSelection().start().toString() );
}
UTEST( TextDocument, autoCloseBrackets ) {
TextDocument doc;
doc.setAutoCloseBrackets( true );
// Test word boundary
doc.insert( 0, { 0, 0 }, "word" );
doc.setSelection( { 0, 0 } ); // Before 'word'
doc.textInput( "(" ); // Next char 'w' is a word char, shouldn't auto close
EXPECT_STRINGEQ( "(word\n", doc.line( 0 ).getText() );
doc.reset();
doc.setAutoCloseBrackets( true );
doc.insert( 0, { 0, 0 }, " word" );
doc.setSelection( { 0, 0 } ); // Before ' word'
doc.textInput( "(" ); // Next char ' ' is not a word char, should auto close
EXPECT_STRINGEQ( "() word\n", doc.line( 0 ).getText() );
doc.reset();
doc.setAutoCloseBrackets( true );
doc.insert( 0, { 0, 0 }, "() )" );
doc.setSelection( { 0, 1 } ); // Inside first parens
doc.textInput( "(" ); // Unmatched right paren ahead, shouldn't auto close
EXPECT_STRINGEQ( "(() )\n", doc.line( 0 ).getText() );
doc.reset();
doc.setAutoCloseBrackets( true );
doc.insert( 0, { 0, 0 }, "()" );
doc.setSelection( { 0, 1 } ); // Inside first parens
doc.textInput( "(" ); // Balanced right paren ahead, should auto close
EXPECT_STRINGEQ( "(())\n", doc.line( 0 ).getText() );
doc.reset();
doc.setAutoCloseBrackets( true );
doc.insert( 0, { 0, 0 }, "(\"\")" );
doc.setSelection( { 0, 2 } ); // Inside quotes
doc.textInput( "\"" ); // Overwrites existing quote (stepping over)
EXPECT_STRINGEQ( "(\"\")\n", doc.line( 0 ).getText() );
doc.reset();
doc.setAutoCloseBrackets( true );
doc.insert( 0, { 0, 0 }, "()" );
doc.setSelection( { 0, 1 } ); // Inside parens
doc.textInput( "\"" ); // Balanced quotes (0), should auto close
EXPECT_STRINGEQ( "(\"\")\n", doc.line( 0 ).getText() );
}