Skip to content

Commit 4124f52

Browse files
committed
Generate engine API suggestions header from Lua bindings and C++ headers
1 parent dae91ab commit 4124f52

8 files changed

Lines changed: 1054 additions & 51 deletions

CMakeLists.txt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ set(EDITOR_DIR editor)
4141
# Find OpenGL
4242
find_package(OpenGL REQUIRED)
4343
find_package(Threads REQUIRED)
44+
find_package(Python3 REQUIRED COMPONENTS Interpreter)
4445

4546
set(BACKEND_SOURCES)
4647
set(BACKEND_LIBS)
@@ -106,6 +107,24 @@ add_custom_command(
106107
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
107108
)
108109

110+
# Generate engine API suggestions header from Lua bindings and C++ headers
111+
set(BINDING_DIR "${CMAKE_SOURCE_DIR}/engine/core/script/binding")
112+
set(ENGINE_CORE_DIR "${CMAKE_SOURCE_DIR}/engine/core")
113+
set(API_SUGGESTIONS_HEADER "${GENERATED_DIR}/engine_api_suggestions.h")
114+
set(GENERATE_API_SCRIPT "${CMAKE_SOURCE_DIR}/generate_api_suggestions.py")
115+
116+
file(GLOB BINDING_FILES "${BINDING_DIR}/*.cpp")
117+
file(GLOB_RECURSE ENGINE_HEADERS "${ENGINE_CORE_DIR}/*.h")
118+
119+
add_custom_command(
120+
OUTPUT ${API_SUGGESTIONS_HEADER}
121+
COMMAND ${Python3_EXECUTABLE} ${GENERATE_API_SCRIPT}
122+
${BINDING_DIR} ${API_SUGGESTIONS_HEADER} ${ENGINE_CORE_DIR}
123+
DEPENDS ${GENERATE_API_SCRIPT} ${BINDING_FILES} ${ENGINE_HEADERS}
124+
COMMENT "Generating engine_api_suggestions.h from Lua bindings and C++ headers"
125+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
126+
)
127+
109128
include_directories (${CMAKE_CURRENT_SOURCE_DIR})
110129

111130
include_directories (${DORIAX_ROOT}/libs/sokol)
@@ -164,6 +183,7 @@ if(UNIX AND NOT APPLE)
164183
endif()
165184

166185
add_custom_target(generate_shaders DEPENDS ${SHADERS_HEADER})
186+
add_custom_target(generate_api_suggestions DEPENDS ${API_SUGGESTIONS_HEADER})
167187

168188
add_executable(
169189
doriax-editor
@@ -259,7 +279,7 @@ add_executable(
259279
${BACKEND_SOURCES}
260280
)
261281

262-
add_dependencies(doriax-editor generate_shaders)
282+
add_dependencies(doriax-editor generate_shaders generate_api_suggestions)
263283

264284
# Include directories
265285
target_include_directories(doriax-editor PRIVATE

editor/window/CodeEditor.cpp

Lines changed: 295 additions & 1 deletion
Large diffs are not rendered by default.

editor/window/CodeEditor.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,23 @@ namespace doriax::editor {
4141
bool windowFocused;
4242
EditorInstance* lastFocused;
4343

44+
std::thread symbolParseThread;
45+
std::atomic<bool> isParsingSymbols{false};
46+
std::mutex parsedSymbolsMutex;
47+
std::vector<CustomTextEditor::ProjectSymbol> newLuaSymbols;
48+
std::vector<CustomTextEditor::ProjectSymbol> newCppSymbols;
49+
std::atomic<bool> newSymbolsReady{false};
50+
4451
void checkFileChanges(EditorInstance& instance);
4552
bool loadFileContent(EditorInstance& instance);
4653
void handleFileChangePopup();
4754
std::string getWindowTitle(const EditorInstance& instance) const;
4855
void updateScriptProperties(const EditorInstance& instance, const std::string& inMemoryContent = "");
56+
57+
// Background parsing for project symbols
58+
void updateAllProjectSymbols();
59+
void applyParsedProjectSymbols();
60+
4961
fs::path resolveFilepath(const fs::path& relPath) const;
5062
std::string toRelativePath(const std::string& filepath) const;
5163
void insertLuaEntityProperty(EditorInstance& instance, Entity entity, uint32_t entitySceneId);

editor/window/widget/CustomTextEditor.cpp

Lines changed: 178 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "CustomTextEditor.h"
22
#include "SemanticSuggestions.h"
3+
#include "engine_api_suggestions.h"
34
#include "external/IconsFontAwesome6.h"
45
#include "util/UIUtils.h"
56
#include "App.h"
@@ -10,6 +11,7 @@
1011
#include <numeric>
1112
#include <sstream>
1213
#include <cstring>
14+
#include <regex>
1315

1416
#ifdef _WIN32
1517
#include <Windows.h>
@@ -118,10 +120,10 @@ void CustomTextEditor::initializeLanguage() {
118120
"string", "vector", "map", "unordered_map", "set", "unordered_set",
119121
"array", "list", "deque", "queue", "stack", "pair", "tuple",
120122
"unique_ptr", "shared_ptr", "weak_ptr",
121-
"function", "optional", "variant", "any",
122-
"Entity", "Scene", "Transform", "Vector2", "Vector3", "Vector4",
123-
"Matrix4", "Quaternion", "Color", "Rect"
123+
"function", "optional", "variant", "any"
124124
};
125+
// Add engine types from auto-generated header
126+
for (const auto& t : getEngineTypeNames()) languageDef.types.insert(t);
125127
languageDef.singleLineComment = "//";
126128
languageDef.multiLineCommentStart = "/*";
127129
languageDef.multiLineCommentEnd = "*/";
@@ -138,13 +140,18 @@ void CustomTextEditor::initializeLanguage() {
138140
"string", "number", "boolean", "table", "function", "thread",
139141
"userdata"
140142
};
143+
// Add engine types from auto-generated header
144+
for (const auto& t : getEngineTypeNames()) languageDef.types.insert(t);
141145
languageDef.builtinFunctions = {
142146
"assert", "collectgarbage", "dofile", "error", "getmetatable",
143147
"ipairs", "load", "loadfile", "next", "pairs", "pcall", "print",
144148
"rawequal", "rawget", "rawlen", "rawset", "require", "select",
145149
"setmetatable", "tonumber", "tostring", "type", "xpcall",
146150
"coroutine", "debug", "io", "math", "os", "package", "string", "table", "utf8"
147151
};
152+
// Add engine builtins (static classes + enums) from auto-generated header
153+
for (const auto& b : getEngineBuiltinNames()) languageDef.builtinFunctions.insert(b);
154+
for (const auto& e : getEngineEnumNames()) languageDef.builtinFunctions.insert(e);
148155
languageDef.singleLineComment = "--";
149156
languageDef.multiLineCommentStart = "--[[";
150157
languageDef.multiLineCommentEnd = "]]";
@@ -277,6 +284,48 @@ void CustomTextEditor::initializeSuggestions() {
277284
suggestions->AddSnippet("cmake_min", "cmake_minimum_required(VERSION ${1:3.16})", "cmake minimum version");
278285
suggestions->AddSnippet("project", "project(${1:name}\n\tVERSION ${2:1.0.0}\n\tLANGUAGES ${3:CXX}\n)", "project definition");
279286
}
287+
288+
// Add engine API suggestions for Lua and C++
289+
if (language == SyntaxLanguage::Lua || language == SyntaxLanguage::Cpp) {
290+
addEngineAPISuggestions();
291+
}
292+
}
293+
294+
void CustomTextEditor::addEngineAPISuggestions() {
295+
if (!suggestions) return;
296+
297+
static const auto& apiSymbols = getEngineAPISymbols();
298+
299+
static const std::unordered_map<std::string, SuggestionKind> kindMap = {
300+
{"Class", SuggestionKind::Class},
301+
{"Enum", SuggestionKind::Enum},
302+
{"EnumMember", SuggestionKind::EnumMember},
303+
{"Method", SuggestionKind::Method},
304+
{"Function", SuggestionKind::Function},
305+
{"Property", SuggestionKind::Property},
306+
{"Constant", SuggestionKind::Constant},
307+
{"Variable", SuggestionKind::Variable},
308+
};
309+
310+
for (const auto& sym : apiSymbols) {
311+
SuggestionKind sk = SuggestionKind::Variable;
312+
auto it = kindMap.find(sym.kind);
313+
if (it != kindMap.end()) sk = it->second;
314+
suggestions->AddSymbol(sym.name, sk, sym.detail, sym.parent ? sym.parent : "");
315+
316+
// Build inheritance map from class detail strings like "class Mesh : Object"
317+
if (sk == SuggestionKind::Class && sym.detail) {
318+
std::string detail = sym.detail;
319+
size_t colonPos = detail.find(" : ");
320+
if (colonPos != std::string::npos) {
321+
std::string className = sym.name;
322+
std::string parentName = detail.substr(colonPos + 3);
323+
// Trim any trailing whitespace
324+
while (!parentName.empty() && parentName.back() == ' ') parentName.pop_back();
325+
suggestions->SetClassParent(className, parentName);
326+
}
327+
}
328+
}
280329
}
281330

282331
void CustomTextEditor::SetLanguage(SyntaxLanguage lang) {
@@ -1756,11 +1805,61 @@ char CustomTextEditor::findMatchingBracket(const TextPosition& pos, TextPosition
17561805
return target;
17571806
}
17581807

1808+
std::string CustomTextEditor::inferTypeOfVariable(const std::string& varName, int currentLine) const {
1809+
if (varName.empty()) return "";
1810+
1811+
// C++ patterns:
1812+
// 1. Standard declaration: [const] Type [*&] varName
1813+
std::regex cppDeclRegex("(?:const\\s+)?([A-Za-z0-9_:]+)(?:<[^>]*>)?\\s*(?:[*&]+\\s*)?" + varName + "\\b");
1814+
// 2. auto varName = new Type(...)
1815+
std::regex cppNewRegex("\\bauto\\s*[*&]*\\s*" + varName + "\\s*=\\s*new\\s+([A-Za-z0-9_:]+)");
1816+
// 3. auto varName = Type(...) or auto varName = Type{...}
1817+
std::regex cppAutoCtorRegex("\\bauto\\s*[*&]*\\s*" + varName + "\\s*=\\s*([A-Z][A-Za-z0-9_:]*)\\s*[({]");
1818+
1819+
std::regex luaVarRegex("\\b" + varName + "\\s*=\\s*([A-Za-z0-9_\\.:]+)(?:\\(|\\{)");
1820+
std::regex luaLocalVarRegex("\\blocal\\s+" + varName + "\\s*=\\s*([A-Za-z0-9_\\.:]+)(?:\\(|\\{)");
1821+
1822+
// Search backwards from the current line
1823+
for (int i = currentLine; i >= 0; --i) {
1824+
if (i >= static_cast<int>(lines.size())) continue;
1825+
const std::string& line = lines[i];
1826+
std::smatch match;
1827+
1828+
if (language == SyntaxLanguage::Cpp) {
1829+
// Try auto = new Type first (most specific)
1830+
if (std::regex_search(line, match, cppNewRegex)) {
1831+
return match[1].str();
1832+
}
1833+
// Try auto = Type() / Type{}
1834+
if (std::regex_search(line, match, cppAutoCtorRegex)) {
1835+
return match[1].str();
1836+
}
1837+
// Try standard declaration
1838+
if (std::regex_search(line, match, cppDeclRegex)) {
1839+
std::string typeMatch = match[1].str();
1840+
if (typeMatch != "return" && typeMatch != "new" && typeMatch != "delete" && typeMatch != "auto") {
1841+
return typeMatch;
1842+
}
1843+
}
1844+
} else if (language == SyntaxLanguage::Lua) {
1845+
if (std::regex_search(line, match, luaLocalVarRegex)) {
1846+
return match[1].str();
1847+
}
1848+
if (std::regex_search(line, match, luaVarRegex)) {
1849+
return match[1].str();
1850+
}
1851+
}
1852+
}
1853+
return "";
1854+
}
1855+
17591856
SuggestionContext CustomTextEditor::buildSuggestionContext() const {
17601857
SuggestionContext ctx;
17611858

17621859
if (cursors.empty()) return ctx;
17631860

1861+
ctx.isCpp = (language == SyntaxLanguage::Cpp);
1862+
17641863
const TextPosition& pos = cursors[primaryCursor].position;
17651864

17661865
// Get current word being typed
@@ -1784,22 +1883,52 @@ SuggestionContext CustomTextEditor::buildSuggestionContext() const {
17841883
ctx.afterDot = true;
17851884
} else if (c == '>' && checkCol > 0 && ctx.lineContent[checkCol - 1] == '-') {
17861885
ctx.afterArrow = true;
1886+
checkCol--; // move past '-'
17871887
} else if (c == ':' && checkCol > 0 && ctx.lineContent[checkCol - 1] == ':') {
17881888
ctx.afterDoubleColon = true;
1889+
checkCol--; // move past first ':'
1890+
} else if (c == ':') {
1891+
if (language == SyntaxLanguage::Lua) {
1892+
ctx.afterColon = true;
1893+
}
17891894
}
17901895
}
17911896
}
17921897

1793-
// Get previous word for context
1794-
if (wordStart.column > 1) {
1795-
TextPosition prevEnd(pos.line, wordStart.column - 1);
1796-
// Skip whitespace
1797-
while (prevEnd.column > 0 && std::isspace(ctx.lineContent[prevEnd.column - 1])) {
1898+
// Get previous word for context (e.g. before the dot)
1899+
if (checkCol > 0) {
1900+
TextPosition prevEnd(pos.line, checkCol - 1);
1901+
// Skip whitespace if any
1902+
while (prevEnd.column > 0 && std::isspace(ctx.lineContent[prevEnd.column])) {
17981903
prevEnd.column--;
17991904
}
1800-
if (prevEnd.column > 0) {
1801-
TextPosition prevStart = findWordStart(prevEnd);
1802-
ctx.previousWord = getRange(prevStart, prevEnd);
1905+
if (prevEnd.column >= 0 && (std::isalnum(ctx.lineContent[prevEnd.column]) || ctx.lineContent[prevEnd.column] == '_')) {
1906+
TextPosition prevStart = findWordStart(TextPosition(pos.line, prevEnd.column + 1));
1907+
// getRange uses end column generically as exclusive bound, so we use prevEnd.column+1
1908+
ctx.previousWord = getRange(prevStart, TextPosition(pos.line, prevEnd.column + 1));
1909+
}
1910+
}
1911+
1912+
// Infer the type of the previous word for semantic suggestions
1913+
if (ctx.afterDot || ctx.afterArrow || ctx.afterDoubleColon || ctx.afterColon) {
1914+
if (!ctx.previousWord.empty()) {
1915+
if (ctx.afterDoubleColon) {
1916+
// For ::, the previousWord IS the type name (e.g. Vector3::ZERO)
1917+
ctx.targetType = ctx.previousWord;
1918+
} else {
1919+
ctx.targetType = inferTypeOfVariable(ctx.previousWord, pos.line);
1920+
// Fallback: look up the variable in project symbols (e.g. header member variables)
1921+
if (ctx.targetType.empty() && suggestions) {
1922+
ctx.targetType = suggestions->FindSymbolType(ctx.previousWord);
1923+
}
1924+
}
1925+
// Strip namespace prefix for matching engine API parent types (e.g. "doriax::Mesh" -> "Mesh")
1926+
if (!ctx.targetType.empty()) {
1927+
size_t lastColon = ctx.targetType.rfind(':');
1928+
if (lastColon != std::string::npos) {
1929+
ctx.targetType = ctx.targetType.substr(lastColon + 1);
1930+
}
1931+
}
18031932
}
18041933
}
18051934

@@ -1821,6 +1950,23 @@ void CustomTextEditor::CloseAutoComplete() {
18211950
autoCompleteItems.clear();
18221951
}
18231952

1953+
void CustomTextEditor::UpdateProjectSymbols(const std::vector<ProjectSymbol>& symbols) {
1954+
if (!suggestions) return;
1955+
1956+
suggestions->ClearSymbols();
1957+
suggestions->ClearInheritance();
1958+
1959+
// Re-add engine API symbols
1960+
if (language == SyntaxLanguage::Lua || language == SyntaxLanguage::Cpp) {
1961+
addEngineAPISuggestions();
1962+
}
1963+
1964+
// Add pre-parsed project symbols
1965+
for (const auto& sym : symbols) {
1966+
suggestions->AddSymbol(sym.name, static_cast<SuggestionKind>(sym.kind), sym.detail, sym.parentType, sym.typeInfo);
1967+
}
1968+
}
1969+
18241970
void CustomTextEditor::updateSuggestions() {
18251971
if (!suggestions) return;
18261972

@@ -2715,7 +2861,21 @@ void CustomTextEditor::handleTextInput() {
27152861
}
27162862

27172863
// Update auto-complete
2718-
if (autoComplete && std::isalnum(c)) {
2864+
bool shouldTrigger = false;
2865+
if (autoComplete) {
2866+
if (std::isalnum(c) || c == '_' || c == '.' || c == ':') {
2867+
shouldTrigger = true;
2868+
} else if (c == '>' && language == SyntaxLanguage::Cpp) {
2869+
// Trigger for '->' operator
2870+
const auto& pos = cursors[primaryCursor].position;
2871+
if (pos.column >= 2 && pos.line < static_cast<int>(lines.size())) {
2872+
if (lines[pos.line][pos.column - 2] == '-') {
2873+
shouldTrigger = true;
2874+
}
2875+
}
2876+
}
2877+
}
2878+
if (shouldTrigger) {
27192879
TriggerAutoComplete();
27202880
} else if (showAutoComplete) {
27212881
// Close autocomplete when typing non-identifier characters
@@ -3172,6 +3332,12 @@ void CustomTextEditor::Render(const char* title, const ImVec2& size, bool border
31723332
if (contentSize.y == 0) contentSize.y = ImGui::GetContentRegionAvail().y;
31733333

31743334
if (ImGui::BeginChild(title, contentSize, border ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, flags)) {
3335+
// Auto-focus editor child when parent window is focused but child is not
3336+
// (e.g., user clicked the window tab/title bar)
3337+
if (!ImGui::IsWindowFocused() && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
3338+
ImGui::SetWindowFocus();
3339+
}
3340+
31753341
if (pendingFocus) {
31763342
ImGui::SetWindowFocus();
31773343
pendingFocus = false;

editor/window/widget/CustomTextEditor.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,17 @@ namespace doriax::editor {
200200
void CloseAutoComplete();
201201
bool IsAutoCompleteOpen() const { return showAutoComplete; }
202202

203+
struct ProjectSymbol {
204+
std::string name;
205+
int kind; // SuggestionKind cast to int
206+
std::string detail;
207+
std::string parentType; // Class/type this belongs to (if any)
208+
std::string typeInfo; // Declared type (for member variables, e.g. "Mesh" for "Mesh* box4")
209+
};
210+
211+
// Project symbols (scanned from sibling files at runtime)
212+
void UpdateProjectSymbols(const std::vector<ProjectSymbol>& symbols);
213+
203214
// Tooltip
204215
void ShowTooltip(const std::string& text, const ImVec2& pos);
205216
void HideTooltip();
@@ -309,10 +320,12 @@ namespace doriax::editor {
309320
void initializeLanguage();
310321
void initializePalette();
311322
void initializeSuggestions();
323+
void addEngineAPISuggestions();
312324
void updateSuggestions();
313325
void applySuggestion();
314326
void renderSuggestions(const ImVec2& origin);
315327
SuggestionContext buildSuggestionContext() const;
328+
std::string inferTypeOfVariable(const std::string& varName, int currentLine) const;
316329
void tokenizeLine(int lineIndex);
317330
void tokenizeAll();
318331
TokenType classifyWord(const std::string& word) const;

0 commit comments

Comments
 (0)