Add configurable Tab Out support

Add an optional Tab Out behavior that moves a single cursor past configured trailing characters when pressing Tab. Expose the feature in Global Document Settings, keep it disabled by default, persist its configuration, and preserve existing selection and multi-cursor indentation behavior (SpartanJ/ecode#951).
This commit is contained in:
Martín Lucas Golini
2026-08-21 11:35:35 -03:00
parent 2895d771ab
commit 8eed5fcd58
13 changed files with 159 additions and 0 deletions

View File

@@ -965,4 +965,8 @@ Verwendet strftime-Formatbezeichner. Die Dateierweiterung richtet sich nach dem
<string name="reasoning_off">Aus</string>
<string name="reasoning_default">Standard</string>
<string name="reasoning_on">Ein</string>
<string name="tab_out">Tab-Ausgang</string>
<string name="tab_out_desc">Wenn Tab vor einem konfigurierten Zeichen gedrückt wird, springt der Cursor darüber. Gilt nur für einen einzelnen Cursor ohne Auswahl.</string>
<string name="set_tab_out_characters">Tab-Ausgangszeichen festlegen</string>
<string name="set_tab_out_characters_message">Legen Sie die Zeichen fest, über die der Cursor beim Drücken von Tab springen kann.</string>
</resources>

View File

@@ -949,4 +949,8 @@ Uses strftime format specifiers. The file extension follows the selected screens
<string name="reasoning_off">Off</string>
<string name="reasoning_default">Default</string>
<string name="reasoning_on">On</string>
<string name="tab_out">Tab Out</string>
<string name="tab_out_desc">Pressing Tab before a configured character moves the cursor past it. Only applies to a single cursor without a selection.</string>
<string name="set_tab_out_characters">Set Tab Out Characters</string>
<string name="set_tab_out_characters_message">Set the characters the cursor can move past when pressing Tab.</string>
</resources>

View File

@@ -944,4 +944,8 @@ Utilise les spécificateurs de format strftime. Lextension du fichier corresp
<string name="reasoning_off">Désactivé</string>
<string name="reasoning_default">Par défaut</string>
<string name="reasoning_on">Activé</string>
<string name="tab_out">Sortie par tabulation</string>
<string name="tab_out_desc">Appuyer sur Tab avant un caractère configuré déplace le curseur après celui-ci. S'applique uniquement à un curseur sans sélection.</string>
<string name="set_tab_out_characters">Définir les caractères de sortie</string>
<string name="set_tab_out_characters_message">Définissez les caractères que le curseur peut franchir en appuyant sur Tab.</string>
</resources>

View File

@@ -723,4 +723,8 @@ file in the directory tree.</string>
<string name="reasoning_off">关闭</string>
<string name="reasoning_default">默认</string>
<string name="reasoning_on">开启</string>
<string name="tab_out">Tab 跳出</string>
<string name="tab_out_desc">在已配置的字符前按 Tab 可将光标移至该字符之后。仅适用于无选区的单个光标。</string>
<string name="set_tab_out_characters">设置 Tab 跳出字符</string>
<string name="set_tab_out_characters_message">设置按 Tab 时光标可以跳过的字符。</string>
</resources>

View File

@@ -564,6 +564,14 @@ class EE_API TextDocument {
const std::vector<std::pair<String::StringBaseType, String::StringBaseType>>&
autoCloseBracketsPairs );
bool getTabOutEnabled() const;
void setTabOutEnabled( bool enabled );
const String& getTabOutChars() const;
void setTabOutChars( const String& chars );
bool isDirtyOnFileSystem() const;
void setDirtyOnFileSystem( bool dirtyOnFileSystem );
@@ -780,6 +788,7 @@ class EE_API TextDocument {
bool mTrimTrailingWhitespaces{ false };
bool mVerbose{ false };
bool mAutoCloseBrackets{ false };
bool mTabOutEnabled{ false };
bool mDirtyOnFileSystem{ false };
bool mSaving{ false };
bool mDeleteOnClose{ false };
@@ -789,6 +798,7 @@ class EE_API TextDocument {
bool mDoingTextInput{ false };
bool mInsertingText{ false };
std::vector<std::pair<String::StringBaseType, String::StringBaseType>> mAutoCloseBracketsPairs;
String mTabOutChars{ ")]}'\":;>," };
Uint32 mIndentWidth{ 4 };
IndentType mIndentType{ IndentType::IndentTabs };
AutoIndentConfig mAutoIndent{ AutoIndentConfig::Smart };

View File

@@ -2906,6 +2906,12 @@ void TextDocument::removeFromStartOfSelectedLines( const String& text, bool skip
}
void TextDocument::indent() {
if ( mTabOutEnabled && mSelection.size() == 1 && !hasSelection() &&
mTabOutChars.find( getCurrentChar() ) != String::InvalidPos ) {
moveToNextChar();
return;
}
if ( hasSelection() ) {
insertAtStartOfSelectedLines( getIndentString(), false );
} else {
@@ -3042,6 +3048,22 @@ void TextDocument::setAutoCloseBracketsPairs(
mAutoCloseBracketsPairs = autoCloseBracketsPairs;
}
bool TextDocument::getTabOutEnabled() const {
return mTabOutEnabled;
}
void TextDocument::setTabOutEnabled( bool enabled ) {
mTabOutEnabled = enabled;
}
const String& TextDocument::getTabOutChars() const {
return mTabOutChars;
}
void TextDocument::setTabOutChars( const String& chars ) {
mTabOutChars = chars;
}
bool TextDocument::isDirtyOnFileSystem() const {
return mDirtyOnFileSystem;
}

View File

@@ -294,6 +294,73 @@ UTEST( TextDocument, newLineNormal ) {
EXPECT_STDSTREQ( TextPosition( 1, 2 ).toString(), doc.getSelection().start().toString() );
}
UTEST( TextDocument, indentTabsOutOfTrailingCharactersWithSingleCursor ) {
static constexpr char trailingCharacters[] = ")]}'\":;>,";
for ( const char trailingCharacter : trailingCharacters ) {
if ( trailingCharacter == '\0' )
break;
TextDocument doc;
doc.setIndentType( TextDocument::IndentType::IndentTabs );
doc.setTabOutEnabled( true );
doc.insert( 0, { 0, 0 }, String( trailingCharacter ) );
doc.setSelection( { 0, 0 } );
doc.indent();
EXPECT_STRINGEQ( String( trailingCharacter ) + "\n", doc.line( 0 ).getText() );
EXPECT_STDSTREQ( TextPosition( 0, 1 ).toString(), doc.getSelection().start().toString() );
}
}
UTEST( TextDocument, indentFallsBackToExistingBehavior ) {
TextDocument doc;
doc.setIndentType( TextDocument::IndentType::IndentTabs );
doc.setTabOutEnabled( true );
doc.insert( 0, { 0, 0 }, ") ordinary" );
// A non-trailing character still inserts indentation.
doc.setSelection( { 0, 2 } );
doc.indent();
EXPECT_STRINGEQ( ") \tordinary\n", doc.line( 0 ).getText() );
// A selection still indents the selected line, even before a trailing character.
doc.setSelection( { { 0, 0 }, { 0, 1 } } );
doc.indent();
EXPECT_STRINGEQ( "\t) \tordinary\n", doc.line( 0 ).getText() );
}
UTEST( TextDocument, indentDoesNotTabOutWithMultipleCursors ) {
TextDocument doc;
doc.setIndentType( TextDocument::IndentType::IndentTabs );
doc.setTabOutEnabled( true );
doc.insert( 0, { 0, 0 }, ")\n}" );
doc.resetSelection( TextRanges( std::vector<TextRange>{ TextRange( { 0, 0 }, { 0, 0 } ),
TextRange( { 1, 0 }, { 1, 0 } ) } ) );
doc.indent();
EXPECT_STRINGEQ( "\t)\n", doc.line( 0 ).getText() );
EXPECT_STRINGEQ( "\t}\n", doc.line( 1 ).getText() );
}
UTEST( TextDocument, indentTabOutIsOptionalAndConfigurable ) {
TextDocument doc;
doc.setIndentType( TextDocument::IndentType::IndentTabs );
doc.insert( 0, { 0, 0 }, ")x" );
doc.setSelection( { 0, 0 } );
// Disabled by default.
doc.indent();
EXPECT_STRINGEQ( "\t)x\n", doc.line( 0 ).getText() );
// A custom set replaces the defaults.
doc.setTabOutEnabled( true );
doc.setTabOutChars( "x" );
doc.setSelection( { 0, 2 } );
doc.indent();
EXPECT_STDSTREQ( TextPosition( 0, 3 ).toString(), doc.getSelection().start().toString() );
}
UTEST( TextDocument, moveToStartOfContent ) {
TextDocument doc;
doc.insert( 0, { 0, 0 }, " content" );

View File

@@ -184,6 +184,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath,
doc.indentWidth = ini.getValueI( "document", "indent_width", 4 );
doc.indentSpaces = ini.getValueB( "document", "indent_spaces", false );
doc.tabStops = ini.getValueB( "document", "tab_stops", true );
doc.tabOutEnabled = ini.getValueB( "document", "tab_out_enabled", false );
doc.tabOutChars = ini.getValue( "document", "tab_out_chars", ")]}'\":;>," );
doc.lineEndings =
TextFormat::stringToLineEnding( ini.getValue( "document", "line_endings", "LF" ) );
editor.newTabPosition =
@@ -395,6 +397,8 @@ void AppConfig::save( const std::vector<std::string>& recentFiles,
ini.setValueB( "document", "write_bom", doc.writeUnicodeBOM );
ini.setValueI( "document", "indent_width", doc.indentWidth );
ini.setValueB( "document", "tab_stops", doc.tabStops );
ini.setValueB( "document", "tab_out_enabled", doc.tabOutEnabled );
ini.setValue( "document", "tab_out_chars", doc.tabOutChars );
ini.setValueB( "document", "indent_spaces", doc.indentSpaces );
ini.setValue( "document", "line_endings", TextFormat::lineEndingToString( doc.lineEndings ) );
ini.setValueI( "document", "tab_width", doc.tabWidth );

View File

@@ -156,8 +156,10 @@ struct DocumentConfig {
bool writeUnicodeBOM{ false };
bool indentSpaces{ false };
bool tabStops{ true };
bool tabOutEnabled{ false };
TextFormat::LineEnding lineEndings{ TextFormat::LineEnding::LF };
TextDocument::AutoIndentConfig autoIndent{ TextDocument::AutoIndentConfig::Smart };
std::string tabOutChars{ ")]}'\":;>," };
int indentWidth{ 4 };
int tabWidth{ 4 };
int lineBreakingColumn{ 100 };

View File

@@ -2985,6 +2985,8 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) {
doc.setAutoCloseBrackets( !mConfig.editor.autoCloseBrackets.empty() );
doc.setAutoCloseBracketsPairs( makeAutoClosePairs( mConfig.editor.autoCloseBrackets ) );
doc.setTabOutEnabled( mConfig.doc.tabOutEnabled );
doc.setTabOutChars( String::fromUtf8( mConfig.doc.tabOutChars ) );
doc.setLineEnding( docc.lineEndings );
doc.setTrimTrailingWhitespaces( docc.trimTrailingWhitespaces );
doc.setForceNewLineAtEndOfFile( docc.forceNewLineAtEndOfFile );

View File

@@ -251,6 +251,25 @@ void SettingsActions::setIndentTabCharacter() {
mApp->setFocusEditorOnClose( msgBox );
}
void SettingsActions::setTabOutChars() {
UIMessageBox* msgBox = UIMessageBox::New(
UIMessageBox::INPUT,
i18n( "set_tab_out_characters_message",
"Set the characters the cursor can move past when pressing Tab." ) );
msgBox->setTitle( i18n( "set_tab_out_characters", "Set Tab Out Characters" ) );
msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } );
msgBox->getTextInput()->setText( String::fromUtf8( mApp->getConfig().doc.tabOutChars ) );
msgBox->showWhenReady();
msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) {
String chars = msgBox->getTextInput()->getText();
mApp->getConfig().doc.tabOutChars = chars.toUtf8();
mApp->getSplitter()->forEachEditor(
[&chars]( UICodeEditor* editor ) { editor->getDocument().setTabOutChars( chars ); } );
msgBox->closeWindow();
} );
mApp->setFocusEditorOnClose( msgBox );
}
void SettingsActions::setFoldRefreshFreq() {
UIMessageBox* msgBox = UIMessageBox::New(
UIMessageBox::INPUT,

View File

@@ -25,6 +25,8 @@ class SettingsActions {
void setIndentTabCharacter();
void setTabOutChars();
void setFoldRefreshFreq();
void setUIScaleFactor();

View File

@@ -824,6 +824,14 @@ UIMenu* SettingsMenu::createDocumentMenu() {
"like a fixed number of spaces regardless of their position." ) )
->setId( "tab_stops" );
mGlobalMenu->addCheckBox( i18n( "tab_out", "Tab Out" ), mApp->getConfig().doc.tabOutEnabled )
->setTooltipText( i18n(
"tab_out_desc", "Pressing Tab before a configured character moves the cursor past it. "
"Only applies to a single cursor without a selection." ) )
->setId( "tab_out" );
mGlobalMenu->add( i18n( "set_tab_out_characters", "Set Tab Out Characters" ) )
->setId( "set_tab_out_characters" );
mGlobalMenu
->addCheckBox( i18n( "force_new_line_at_end_of_file", "Force New Line at End of File" ),
mApp->getConfig().doc.forceNewLineAtEndOfFile )
@@ -948,6 +956,11 @@ UIMenu* SettingsMenu::createDocumentMenu() {
mSplitter->forEachEditor( [this]( UICodeEditor* editor ) {
editor->setTabStops( mApp->getConfig().doc.tabStops );
} );
} else if ( "tab_out" == id ) {
mApp->getConfig().doc.tabOutEnabled = item->isActive();
mSplitter->forEachEditor( [this]( UICodeEditor* editor ) {
editor->getDocument().setTabOutEnabled( mApp->getConfig().doc.tabOutEnabled );
} );
}
} else if ( "line_breaking_column" == id ) {
mApp->getSettingsActions()->setLineBreakingColumn();
@@ -957,6 +970,8 @@ UIMenu* SettingsMenu::createDocumentMenu() {
mApp->getSettingsActions()->setCursorBlinkingTime();
} else if ( "indent_tab_character" == id ) {
mApp->getSettingsActions()->setIndentTabCharacter();
} else if ( "set_tab_out_characters" == id ) {
mApp->getSettingsActions()->setTabOutChars();
}
} );