Much better documentation placement in the auto-complete suggestions function/method documentation (SpartanJ/ecode#874).

This commit is contained in:
Martín Lucas Golini
2026-04-18 13:27:03 -03:00
parent 18be45681d
commit da1bbda8c7
3 changed files with 192 additions and 45 deletions

View File

@@ -171,6 +171,17 @@ void AutoCompletePlugin::load( PluginManager* pluginManager ) {
config["max_label_characters"] = mMaxLabelCharacters;
updateConfigFile = true;
}
if ( config.contains( "max_suggestion_documentation_width" ) )
mMaxSuggestionDocumentationWidth =
config.value( "max_suggestion_documentation_width", "100%" );
else {
config["max_suggestion_documentation_width"] = "100%";
updateConfigFile = true;
}
if ( mMaxSuggestionDocumentationWidth.empty() )
mMaxSuggestionDocumentationWidth = "100%";
}
if ( mKeyBindings.empty() ) {
@@ -1209,33 +1220,33 @@ void AutoCompletePlugin::postDraw( UICodeEditor* editor, const Vector2f& startSc
}
if ( mSuggestionIndex == (int)i && !suggestion.documentation.value.empty() ) {
mSuggestionDoc.setFont( editor->getFont() );
mSuggestionDoc.setFontSize( editor->getFontSize() );
mSuggestionDoc.setFillColor( normalStyle.color );
mSuggestionDoc.setStyle( normalStyle.style );
bool changed = mSuggestionDoc.setString( suggestion.documentation.value );
mSuggestionDoc.setFont( editor->getFont() );
mSuggestionDoc.setFontSize( editor->getFontSize() );
mSuggestionDoc.setLineWrapMode( LineWrapMode::Word );
mSuggestionDoc.setLineWrapKeepIndentation( true );
Vector2f boxPos = { cursorPos.x + mBoxRect.getWidth(),
cursorPos.y + mRowHeight * count };
Sizef boxSize = { mSuggestionDoc.getTextWidth() + mBoxPadding.Left + mBoxPadding.Right,
mSuggestionDoc.getTextHeight() + mBoxPadding.Top +
mBoxPadding.Bottom };
primitives.setColor(
Color( selectedStyle.background ).blendAlpha( editor->getAlpha() ) );
primitives.drawRoundedRectangle( { boxPos, boxSize }, 0.f, Vector2f::One, 6 );
Rectf docRect =
findBestDocumentationPlacement( editor, suggestion, boxRect,
{ { cursorPos.x, cursorPos.y + mRowHeight * count },
{ mBoxRect.getWidth(), mRowHeight } },
drawUp, lineHeight );
if ( changed ) {
bool forceHTML = String::startsWith( suggestion.detail, "Emmet" );
if ( suggestion.documentation.kind == LSPMarkupKind::MarkDown || forceHTML ) {
const auto& syntaxDef =
forceHTML ? SyntaxDefinitionManager::instance()->getByLSPName( "html" )
: SyntaxDefinitionManager::instance()->getByLSPName( "markdown" );
SyntaxTokenizer::tokenizeText( syntaxDef, editor->getColorScheme(),
&mSuggestionDoc, 0, 0xFFFFFFFF, true, "\n\t " );
}
if ( docRect.getSize().getWidth() > 0 && docRect.getSize().getHeight() > 0 ) {
primitives.setColor(
Color( selectedStyle.background ).blendAlpha( editor->getAlpha() ) );
editor->clipSmartEnable( docRect.Left, docRect.Top, docRect.getWidth(),
docRect.getHeight() );
primitives.drawRoundedRectangle( docRect, 0.f, Vector2f::One, 6 );
mSuggestionDoc.draw( docRect.Left + mBoxPadding.Left,
docRect.Top + mBoxPadding.Top );
editor->clipSmartDisable();
}
mSuggestionDoc.draw( boxPos.x + mBoxPadding.Left, boxPos.y + mBoxPadding.Top );
}
count++;
visibleStrIndex++;
@@ -1259,6 +1270,137 @@ void AutoCompletePlugin::postDraw( UICodeEditor* editor, const Vector2f& startSc
(int)eefloor( bar.getWidth() * 0.5f ) );
}
Rectf AutoCompletePlugin::findBestDocumentationPlacement( UICodeEditor* editor,
const Suggestion& suggestion,
const Rectf& anchorBox,
const Rectf& rowRect, bool drawUp,
Float lineHeight ) {
const Rectf& areaRect = editor->getScreenRect();
Float userMaxWidth = editor->convertLength(
StyleSheetLength( mMaxSuggestionDocumentationWidth ), editor->getPixelsSize().getWidth() );
const Float minSideWidth = PixelDensity::dpToPx( 200.f );
const Float margin = PixelDensity::dpToPx( 2.f ); // Visual breathing room
struct PlacementCandidate {
enum Type { Right, Left, Bottom, Top } type;
Float availableWidth;
Float availableHeight;
Float score;
};
// Calculate available space, strictly removing the cursor's line so we don't cover what the
// user is typing.
Float topAvail = std::max( 0.f, ( drawUp ? ( anchorBox.Top - areaRect.Top - lineHeight )
: ( anchorBox.Top - areaRect.Top ) ) -
margin );
Float bottomAvail =
std::max( 0.f, ( drawUp ? ( areaRect.Bottom - anchorBox.Bottom )
: ( areaRect.Bottom - anchorBox.Bottom - lineHeight ) ) -
margin );
Float rightAvail = std::max( 0.f, areaRect.Right - anchorBox.Right - margin );
Float leftAvail = std::max( 0.f, anchorBox.Left - areaRect.Left - margin );
SmallVector<PlacementCandidate, 4> candidates = {
{ PlacementCandidate::Right, rightAvail, std::max( 0.f, areaRect.getHeight() - margin * 2 ),
0 },
{ PlacementCandidate::Left, leftAvail, std::max( 0.f, areaRect.getHeight() - margin * 2 ),
0 },
{ PlacementCandidate::Bottom, std::max( 0.f, areaRect.getWidth() - margin * 2 ),
bottomAvail, 0 },
{ PlacementCandidate::Top, std::max( 0.f, areaRect.getWidth() - margin * 2 ), topAvail,
0 } };
for ( auto& c : candidates ) {
Float maxW =
std::min( userMaxWidth, c.availableWidth - mBoxPadding.Left - mBoxPadding.Right );
if ( maxW < 50 || c.availableHeight < mRowHeight * 2 ) {
c.score = 0;
continue;
}
c.score = maxW * std::min( c.availableHeight, mRowHeight * 10 );
if ( ( c.type == PlacementCandidate::Right || c.type == PlacementCandidate::Left ) &&
c.availableWidth >= minSideWidth ) {
c.score += 1000000; // Prefer side placement if it has enough width
}
}
std::sort( candidates.begin(), candidates.end(),
[]( const auto& a, const auto& b ) { return a.score > b.score; } );
const auto& best = candidates.front();
// Edge case: No space anywhere on screen (zero score)
if ( best.score == 0 ) {
return Rectf(); // Return empty rect, `postDraw` will skip drawing
}
Float maxWidth = std::max(
0.f, std::min( userMaxWidth, best.availableWidth - mBoxPadding.Left - mBoxPadding.Right ) );
mSuggestionDoc.setMaxWrapWidth( maxWidth );
bool changed = mSuggestionDoc.setString( suggestion.documentation.value );
if ( changed ) {
bool forceHTML = String::startsWith( suggestion.detail, "Emmet" );
if ( suggestion.documentation.kind == LSPMarkupKind::MarkDown || forceHTML ) {
const auto& syntaxDef =
forceHTML ? SyntaxDefinitionManager::instance()->getByLSPName( "html" )
: SyntaxDefinitionManager::instance()->getByLSPName( "markdown" );
SyntaxTokenizer::tokenizeText( syntaxDef, editor->getColorScheme(), &mSuggestionDoc, 0,
0xFFFFFFFF, true, "\n\t " );
}
}
Sizef boxSize = { mSuggestionDoc.getTextWidth() + mBoxPadding.Left + mBoxPadding.Right,
mSuggestionDoc.getTextHeight() + mBoxPadding.Top + mBoxPadding.Bottom };
// Height Clamping: Prevent background box from bleeding off-screen.
// Text that overflows this will simply get clipped or run over gracefully.
boxSize.setHeight( std::min( boxSize.getHeight(), best.availableHeight ) );
Vector2f pos;
if ( best.type == PlacementCandidate::Right ) {
pos.x = anchorBox.Right + margin;
pos.y = rowRect.Top;
} else if ( best.type == PlacementCandidate::Left ) {
pos.x = anchorBox.Left - boxSize.getWidth() - margin;
pos.y = rowRect.Top;
} else if ( best.type == PlacementCandidate::Bottom ) {
pos.x = anchorBox.Left;
pos.y = anchorBox.Bottom + margin + ( !drawUp ? lineHeight : 0 );
} else { // Top
pos.x = anchorBox.Left;
pos.y = anchorBox.Top - boxSize.getHeight() - margin - ( drawUp ? lineHeight : 0 );
}
// Final Clamping: Kept firmly inside `areaRect` but enforcing boundaries to prevent overlap
if ( best.type == PlacementCandidate::Right ) {
pos.x = std::min( pos.x, areaRect.Right - boxSize.getWidth() );
pos.x = std::max( pos.x, anchorBox.Right + margin );
pos.y = std::max( areaRect.Top + margin,
std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin ) );
} else if ( best.type == PlacementCandidate::Left ) {
pos.x = std::max( pos.x, areaRect.Left + margin );
pos.x = std::min( pos.x, anchorBox.Left - boxSize.getWidth() - margin );
pos.y = std::max( areaRect.Top + margin,
std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin ) );
} else if ( best.type == PlacementCandidate::Bottom ) {
pos.y = std::min( pos.y, areaRect.Bottom - boxSize.getHeight() - margin );
Float minBottom = anchorBox.Bottom + margin + ( !drawUp ? lineHeight : 0 );
pos.y = std::max( pos.y, minBottom );
pos.x = std::max( areaRect.Left + margin,
std::min( pos.x, areaRect.Right - boxSize.getWidth() - margin ) );
} else { // Top
pos.y = std::max( pos.y, areaRect.Top + margin );
Float maxTop = anchorBox.Top - boxSize.getHeight() - margin - ( drawUp ? lineHeight : 0 );
pos.y = std::min( pos.y, maxTop );
pos.x = std::max( areaRect.Left + margin,
std::min( pos.x, areaRect.Right - boxSize.getWidth() - margin ) );
}
return Rectf( pos, boxSize ).round();
}
bool AutoCompletePlugin::onMouseDown( UICodeEditor* editor, const Vector2i& position,
const Uint32& flags ) {
if ( mSuggestions.empty() || !mSuggestionsEditor || mSuggestionsEditor != editor ||

View File

@@ -45,7 +45,7 @@ class AutoCompletePlugin : public Plugin {
sortText( sortText.empty() ? std::string{ this->text } : std::move( sortText ) ),
range( range ),
insertText( std::move( insertText ) ),
documentation( doc ){};
documentation( doc ) {};
bool operator<( const Suggestion& other ) const { return getCmpStr() < other.getCmpStr(); }
@@ -173,6 +173,7 @@ class AutoCompletePlugin : public Plugin {
String::HashType mConfigHash{ 0 };
std::unordered_map<std::string, std::string> mKeyBindings;
std::unordered_map<std::string, KeyBindings::Shortcut> mShortcuts;
std::string mMaxSuggestionDocumentationWidth;
Float mRowHeight{ 0 };
Rectf mBoxRect;
@@ -220,6 +221,10 @@ class AutoCompletePlugin : public Plugin {
void tryStartSnippetNav( const Suggestion& suggestion, UICodeEditor* editor,
const TextRanges& prevSels );
Rectf findBestDocumentationPlacement( UICodeEditor* editor, const Suggestion& suggestion,
const Rectf& anchorBox, const Rectf& rowRect, bool drawUp,
Float lineHeight );
void updateShortcuts();
};