Skip to content

Commit b05a754

Browse files
committed
Improved code editor suggestions
1 parent b1c9f8d commit b05a754

5 files changed

Lines changed: 298 additions & 220 deletions

File tree

editor/window/CodeEditor.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,16 @@ void editor::CodeEditor::updateAllProjectSymbols() {
154154
if (end > start) {
155155
std::string fname = line.substr(start, end - start);
156156
if (fname != "(" && !fname.empty() && seenLua.insert(fname).second) {
157-
parsedLua.push_back({fname, 2, "project function", "", ""});
157+
// "function Player:update()" / "Player.update" -> method of Player,
158+
// so it only appears in member completion (player:upd...)
159+
size_t sep = fname.find_first_of(".:");
160+
if (sep != std::string::npos && sep > 0 && sep + 1 < fname.size()) {
161+
std::string owner = fname.substr(0, sep);
162+
std::string method = fname.substr(sep + 1);
163+
parsedLua.push_back({method, 9, owner + ":" + method + "()", owner, ""}); // SuggestionKind::Method = 9
164+
} else {
165+
parsedLua.push_back({fname, 2, "project function", "", ""});
166+
}
158167
}
159168
}
160169
}

editor/window/widget/CustomTextEditor.cpp

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

20032017
void 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

editor/window/widget/CustomTextEditor.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ namespace doriax::editor {
198198

199199
// Auto-complete
200200
void SetAutoComplete(bool enable) { autoComplete = enable; }
201-
void TriggerAutoComplete();
201+
void TriggerAutoComplete(bool manualInvoke = false);
202202
void CloseAutoComplete();
203203
bool IsAutoCompleteOpen() const { return showAutoComplete; }
204204

@@ -343,9 +343,10 @@ namespace doriax::editor {
343343
void initializePalette();
344344
void initializeSuggestions();
345345
void addEngineAPISuggestions();
346-
void updateSuggestions();
346+
void updateSuggestions(bool manualInvoke = false);
347347
void applySuggestion();
348348
void renderSuggestions(const ImVec2& origin);
349+
bool isInCommentOrString(const TextPosition& pos) const;
349350
void triggerParamHint();
350351
void updateParamHint();
351352
void closeParamHint();

0 commit comments

Comments
 (0)