@@ -224,7 +224,10 @@ void CustomTextEditor::initializeSuggestions() {
224224
225225 // Configure the suggestions engine
226226 suggestions->SetMinPrefixLength (1 );
227- suggestions->SetMaxSuggestions (15 );
227+ // The popup shows ~10 rows and scrolls; a high cap keeps inherited members
228+ // visible (e.g. Text has 15+ direct members that would otherwise crowd out
229+ // EntityHandle:getEntity etc.)
230+ suggestions->SetMaxSuggestions (200 );
228231 suggestions->SetFuzzyMatching (true );
229232 suggestions->SetCaseSensitive (false );
230233
@@ -1898,11 +1901,18 @@ std::string CustomTextEditor::inferTypeOfVariable(const std::string& varName, in
18981901 }
18991902 }
19001903 } else if (language == SyntaxLanguage::Lua) {
1901- if (std::regex_search (line, match, luaLocalVarRegex)) {
1902- return match[1 ].str ();
1903- }
1904- if (std::regex_search (line, match, luaVarRegex)) {
1905- return match[1 ].str ();
1904+ if (std::regex_search (line, match, luaLocalVarRegex) ||
1905+ std::regex_search (line, match, luaVarRegex)) {
1906+ std::string typeName = match[1 ].str ();
1907+ // Constructor-style call: "local p = Player.new()" / "Player.create()" -> Player
1908+ size_t sep = typeName.find_first_of (" .:" );
1909+ if (sep != std::string::npos) {
1910+ std::string suffix = typeName.substr (sep + 1 );
1911+ if (suffix == " new" || suffix == " create" ) {
1912+ typeName = typeName.substr (0 , sep);
1913+ }
1914+ }
1915+ return typeName;
19061916 }
19071917 }
19081918 }
@@ -1971,6 +1981,9 @@ SuggestionContext CustomTextEditor::buildSuggestionContext() const {
19711981 if (ctx.afterDoubleColon ) {
19721982 // For ::, the previousWord IS the type name (e.g. Vector3::ZERO)
19731983 ctx.targetType = ctx.previousWord ;
1984+ } else if (suggestions && suggestions->IsKnownClassOrEnum (ctx.previousWord )) {
1985+ // Static access on a class/enum name itself (e.g. Engine.setScene, Scaling.NATIVE)
1986+ ctx.targetType = ctx.previousWord ;
19741987 } else {
19751988 ctx.targetType = inferTypeOfVariable (ctx.previousWord , pos.line );
19761989 // Fallback: look up the variable in project symbols (e.g. header member variables)
@@ -1991,13 +2004,14 @@ SuggestionContext CustomTextEditor::buildSuggestionContext() const {
19912004 return ctx;
19922005}
19932006
1994- void CustomTextEditor::TriggerAutoComplete () {
2007+ void CustomTextEditor::TriggerAutoComplete (bool manualInvoke ) {
19952008 if (!autoComplete || readOnly || !suggestions) return ;
19962009
19972010 autoCompleteAnchor = cursors[primaryCursor].position ;
1998- updateSuggestions ();
2011+ updateSuggestions (manualInvoke );
19992012 showAutoComplete = !currentSuggestions.empty ();
20002013 suggestionIndex = 0 ;
2014+ scrollToSuggestion = true ; // scroll back to top when the popup opens or refilters
20012015}
20022016
20032017void CustomTextEditor::CloseAutoComplete () {
@@ -2080,14 +2094,19 @@ void CustomTextEditor::triggerParamHint() {
20802094 int varStart = varEnd + 1 ;
20812095 while (varStart > 0 && (std::isalnum (funcLine[varStart - 1 ]) || funcLine[varStart - 1 ] == ' _' )) varStart--;
20822096 std::string varName = funcLine.substr (varStart, varEnd - varStart + 1 );
2083- parentType = inferTypeOfVariable (varName, searchLine);
2084- if (parentType.empty () && suggestions) {
2085- parentType = suggestions->FindSymbolType (varName);
2086- }
2087- // Strip namespace
2088- size_t lastColon = parentType.rfind (' :' );
2089- if (lastColon != std::string::npos) {
2090- parentType = parentType.substr (lastColon + 1 );
2097+ if (suggestions && suggestions->IsKnownClassOrEnum (varName)) {
2098+ // Static call on a class name itself (e.g. Engine.setScene)
2099+ parentType = varName;
2100+ } else {
2101+ parentType = inferTypeOfVariable (varName, searchLine);
2102+ if (parentType.empty () && suggestions) {
2103+ parentType = suggestions->FindSymbolType (varName);
2104+ }
2105+ // Strip namespace
2106+ size_t lastColon = parentType.rfind (' :' );
2107+ if (lastColon != std::string::npos) {
2108+ parentType = parentType.substr (lastColon + 1 );
2109+ }
20912110 }
20922111 }
20932112 } else if (accessor == ' >' && beforeFunc > 0 && funcLine[beforeFunc - 1 ] == ' -' ) {
@@ -2369,24 +2388,73 @@ void CustomTextEditor::UpdateProjectSymbols(const std::vector<ProjectSymbol>& sy
23692388 addEngineAPISuggestions ();
23702389 }
23712390
2391+ // Engine API symbols are authoritative (real signatures). Engine headers may live
2392+ // inside the project tree, so the project scanner can produce duplicates of the
2393+ // same members with a generic "project function" detail — skip those.
2394+ static const std::unordered_set<std::string> engineKeys = [] {
2395+ std::unordered_set<std::string> keys;
2396+ for (const auto & sym : getEngineAPISymbols ()) {
2397+ keys.insert (std::string (sym.name ) + " \x1f " + (sym.parent ? sym.parent : " " ));
2398+ }
2399+ return keys;
2400+ }();
2401+
23722402 // Add pre-parsed project symbols
23732403 for (const auto & sym : symbols) {
2404+ if (engineKeys.find (sym.name + " \x1f " + sym.parentType ) != engineKeys.end ()) continue ;
23742405 suggestions->AddSymbol (sym.name , static_cast <SuggestionKind>(sym.kind ), sym.detail , sym.parentType , sym.typeInfo );
23752406 }
23762407}
23772408
2378- void CustomTextEditor::updateSuggestions () {
2379- if (!suggestions) return ;
2409+ bool CustomTextEditor::isInCommentOrString (const TextPosition& pos) const {
2410+ if (pos.line < 0 || pos.line >= static_cast <int >(lineTokens.size ())) return false ;
2411+
2412+ // Look at the character just typed (left of the cursor)
2413+ int col = pos.column > 0 ? pos.column - 1 : 0 ;
2414+ for (const auto & token : lineTokens[pos.line ]) {
2415+ if (col >= token.start && col < token.start + token.length ) {
2416+ return token.type == TokenType::Comment ||
2417+ token.type == TokenType::MultiLineComment ||
2418+ token.type == TokenType::String;
2419+ }
2420+ }
2421+ return false ;
2422+ }
2423+
2424+ void CustomTextEditor::updateSuggestions (bool manualInvoke) {
2425+ if (!suggestions || cursors.empty ()) return ;
2426+
2427+ // No suggestions inside comments or string literals
2428+ if (isInCommentOrString (cursors[primaryCursor].position )) {
2429+ currentSuggestions.clear ();
2430+ autoCompleteItems.clear ();
2431+ showAutoComplete = false ;
2432+ return ;
2433+ }
23802434
23812435 // Update document words in the suggestions engine
23822436 suggestions->UpdateDocumentWords (lines);
23832437
23842438 // Build context and get suggestions
23852439 SuggestionContext ctx = buildSuggestionContext ();
2440+ ctx.manualInvoke = manualInvoke;
23862441 autoCompleteAnchor = findWordStart (cursors[primaryCursor].position );
23872442
23882443 currentSuggestions = suggestions->GetSuggestions (ctx);
23892444
2445+ // Close the popup if filtering (e.g. backspace) left nothing to show
2446+ if (showAutoComplete && currentSuggestions.empty ()) {
2447+ showAutoComplete = false ;
2448+ }
2449+
2450+ // Every content refilter (typing forward or backspace) re-ranks the list,
2451+ // so snap the selection back to the best match at the top and scroll it into
2452+ // view — matching VSCode, where each keystroke selects the top result.
2453+ if (!currentSuggestions.empty ()) {
2454+ suggestionIndex = 0 ;
2455+ scrollToSuggestion = true ;
2456+ }
2457+
23902458 // Also populate the old autoCompleteItems for backward compatibility
23912459 autoCompleteItems.clear ();
23922460 for (const auto & sugg : currentSuggestions) {
@@ -3061,9 +3129,9 @@ void CustomTextEditor::handleKeyboardInput() {
30613129 }
30623130 }
30633131
3064- // Trigger auto-complete with Ctrl+Space
3132+ // Trigger auto-complete with Ctrl+Space (manual invoke shows the full list)
30653133 if (ctrl && ImGui::IsKeyPressed (ImGuiKey_Space)) {
3066- TriggerAutoComplete ();
3134+ TriggerAutoComplete (true );
30673135 }
30683136
30693137 // Update param hint after any cursor movement
0 commit comments