#include "XCUIAssetDocumentSource.h" #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace XCUIBackend { namespace fs = std::filesystem; namespace { using XCEngine::Containers::String; using XCEngine::Resources::CompileUIDocument; using XCEngine::Resources::ResourceManager; using XCEngine::Resources::UIDocumentCompileRequest; using XCEngine::Resources::UIDocumentCompileResult; using XCEngine::Resources::UIDocumentKind; using XCEngine::Resources::UIDocumentResource; using XCEngine::Resources::UITheme; using XCEngine::Resources::UIView; String ToContainersString(const std::string& value) { return String(value.c_str()); } std::string ToStdString(const String& value) { return std::string(value.CStr()); } std::string ToGenericString(const fs::path& path) { return path.lexically_normal().generic_string(); } bool PathExists(const fs::path& path) { std::error_code ec; return !path.empty() && fs::exists(path, ec) && !fs::is_directory(path, ec); } bool TryGetWriteTime( const fs::path& path, fs::file_time_type& outWriteTime) { std::error_code ec; if (path.empty() || !fs::exists(path, ec) || fs::is_directory(path, ec)) { return false; } outWriteTime = fs::last_write_time(path, ec); return !ec; } std::string SanitizeSetName(const std::string& setName) { std::string sanitized = {}; sanitized.reserve(setName.size()); for (unsigned char ch : setName) { if (std::isalnum(ch) != 0 || ch == '_' || ch == '-') { sanitized.push_back(static_cast(ch)); } } return sanitized.empty() ? std::string("Default") : sanitized; } std::string ToSnakeCase(const std::string& value) { std::string snake = {}; snake.reserve(value.size() + 8u); char previous = '\0'; for (unsigned char rawCh : value) { if (!(std::isalnum(rawCh) != 0)) { if (!snake.empty() && snake.back() != '_') { snake.push_back('_'); } previous = '_'; continue; } const char ch = static_cast(rawCh); const bool isUpper = std::isupper(rawCh) != 0; const bool previousIsLowerOrDigit = std::islower(static_cast(previous)) != 0 || std::isdigit(static_cast(previous)) != 0; if (isUpper && !snake.empty() && previousIsLowerOrDigit && snake.back() != '_') { snake.push_back('_'); } snake.push_back(static_cast(std::tolower(rawCh))); previous = ch; } while (!snake.empty() && snake.back() == '_') { snake.pop_back(); } return snake.empty() ? std::string("default") : snake; } std::optional GetCurrentPath() { std::error_code ec; const fs::path currentPath = fs::current_path(ec); if (ec) { return std::nullopt; } return currentPath; } fs::path GetConfiguredResourceRoot() { const String resourceRoot = ResourceManager::Get().GetResourceRoot(); if (resourceRoot.Empty()) { return fs::path(); } return fs::path(resourceRoot.CStr()).lexically_normal(); } std::optional FindKnownDocumentUnderRoot( const fs::path& root, const XCUIAssetDocumentSource::PathSet& paths) { if (PathExists(root / paths.view.primaryRelativePath)) { return paths.view.primaryRelativePath; } if (PathExists(root / paths.theme.primaryRelativePath)) { return paths.theme.primaryRelativePath; } if (PathExists(root / paths.view.legacyRelativePath)) { return paths.view.legacyRelativePath; } if (PathExists(root / paths.theme.legacyRelativePath)) { return paths.theme.legacyRelativePath; } return std::nullopt; } void AppendUniqueSearchRoot( const fs::path& searchRoot, std::vector& outRoots, std::unordered_set& seenRoots) { if (searchRoot.empty()) { return; } const fs::path normalized = searchRoot.lexically_normal(); const std::string key = ToGenericString(normalized); if (!seenRoots.insert(key).second) { return; } outRoots.push_back(normalized); } std::vector BuildRepositoryRootSearchRoots( const std::vector& explicitSearchRoots, bool includeDefaultSearchRoots) { std::vector searchRoots = {}; std::unordered_set seenRoots = {}; for (const fs::path& explicitSearchRoot : explicitSearchRoots) { AppendUniqueSearchRoot(explicitSearchRoot, searchRoots, seenRoots); } if (!includeDefaultSearchRoots) { return searchRoots; } #ifdef XCENGINE_NEW_EDITOR_REPO_ROOT AppendUniqueSearchRoot( fs::path(XCENGINE_NEW_EDITOR_REPO_ROOT), searchRoots, seenRoots); #endif const fs::path resourceRoot = GetConfiguredResourceRoot(); if (!resourceRoot.empty()) { AppendUniqueSearchRoot(resourceRoot, searchRoots, seenRoots); } const std::optional currentPath = GetCurrentPath(); if (currentPath.has_value()) { AppendUniqueSearchRoot(*currentPath, searchRoots, seenRoots); } return searchRoots; } void AddCandidate( std::vector& candidates, std::unordered_set& seenPaths, const std::string& requestPath, const fs::path& resolvedPath, XCUIAssetDocumentSource::PathOrigin origin) { const std::string key = ToGenericString(resolvedPath); if (requestPath.empty() || resolvedPath.empty() || !seenPaths.insert(key).second) { return; } XCUIAssetDocumentSource::ResolutionCandidate candidate = {}; candidate.requestPath = requestPath; candidate.resolvedPath = resolvedPath.lexically_normal(); candidate.origin = origin; candidates.push_back(std::move(candidate)); } void AddRelativeCandidateIfReachable( const std::string& relativePath, const fs::path& repositoryRoot, const fs::path& resourceRoot, XCUIAssetDocumentSource::PathOrigin origin, std::vector& candidates, std::unordered_set& seenPaths) { if (relativePath.empty()) { return; } const fs::path relative(relativePath); if (PathExists(relative)) { AddCandidate(candidates, seenPaths, relativePath, relative, origin); return; } if (!resourceRoot.empty() && PathExists(resourceRoot / relative)) { AddCandidate(candidates, seenPaths, relativePath, resourceRoot / relative, origin); return; } if (!repositoryRoot.empty() && PathExists(repositoryRoot / relative)) { AddCandidate( candidates, seenPaths, (repositoryRoot / relative).generic_string(), repositoryRoot / relative, origin); } } template bool TryLoadDocumentFromResourceManager( const std::string& requestPath, UIDocumentCompileResult& outResult) { static_assert( std::is_base_of_v, "TDocumentResource must derive from UIDocumentResource"); auto resource = ResourceManager::Get().Load(ToContainersString(requestPath)); if (!resource || resource->GetDocument().valid == false) { return false; } outResult = UIDocumentCompileResult(); outResult.document = resource->GetDocument(); outResult.succeeded = outResult.document.valid; return outResult.succeeded; } bool TryLoadDocumentFromResourceManager( UIDocumentKind kind, const std::string& requestPath, UIDocumentCompileResult& outResult) { switch (kind) { case UIDocumentKind::View: return TryLoadDocumentFromResourceManager(requestPath, outResult); case UIDocumentKind::Theme: return TryLoadDocumentFromResourceManager(requestPath, outResult); default: return false; } } bool TryCompileDocumentDirect( const XCUIAssetDocumentSource::DocumentPathSpec& spec, const std::string& requestPath, UIDocumentCompileResult& outResult) { UIDocumentCompileRequest request = {}; request.kind = spec.kind; request.path = ToContainersString(requestPath); request.expectedRootTag = ToContainersString(spec.expectedRootTag); return CompileUIDocument(request, outResult) && outResult.succeeded; } void CollectTrackedSourcePaths( const XCUIAssetDocumentSource::DocumentLoadState& documentState, std::vector& outPaths, std::unordered_set& seenPaths) { if (!documentState.succeeded) { return; } auto pushPath = [&](const String& path) { if (path.Empty()) { return; } const std::string text = ToStdString(path); if (seenPaths.insert(text).second) { outPaths.push_back(text); } }; if (!documentState.sourcePath.empty() && seenPaths.insert(documentState.sourcePath).second) { outPaths.push_back(documentState.sourcePath); } if (!documentState.compileResult.document.valid) { return; } pushPath(documentState.compileResult.document.sourcePath); for (const String& dependency : documentState.compileResult.document.dependencies) { pushPath(dependency); } } void UpdateTrackedWriteTimes( const XCUIAssetDocumentSource::LoadState& state, std::vector& outTrackedWriteTimes, std::vector& outMissingTrackedSourcePaths, bool& outChangeTrackingReady, std::string& outTrackingStatusMessage) { outTrackedWriteTimes.clear(); outMissingTrackedSourcePaths.clear(); outTrackedWriteTimes.reserve(state.trackedSourcePaths.size()); outMissingTrackedSourcePaths.reserve(state.trackedSourcePaths.size()); for (const std::string& pathText : state.trackedSourcePaths) { XCUIAssetDocumentSource::TrackedWriteTime tracked = {}; tracked.path = fs::path(pathText).lexically_normal(); if (!TryGetWriteTime(tracked.path, tracked.writeTime)) { outMissingTrackedSourcePaths.push_back(ToGenericString(tracked.path)); continue; } outTrackedWriteTimes.push_back(std::move(tracked)); } outChangeTrackingReady = !state.trackedSourcePaths.empty() && outTrackedWriteTimes.size() == state.trackedSourcePaths.size(); if (state.trackedSourcePaths.empty()) { outTrackingStatusMessage = "No XCUI source files were recorded for hot reload."; return; } if (outMissingTrackedSourcePaths.empty()) { outTrackingStatusMessage = "Tracking " + std::to_string(static_cast(outTrackedWriteTimes.size())) + " XCUI source file(s) for hot reload."; return; } outTrackingStatusMessage = "Tracking " + std::to_string(static_cast(outTrackedWriteTimes.size())) + " of " + std::to_string(static_cast(state.trackedSourcePaths.size())) + " XCUI source file(s); unable to stat " + std::to_string(static_cast(outMissingTrackedSourcePaths.size())) + " path(s)."; } XCUIAssetDocumentSource::DocumentLoadState LoadDocument( const XCUIAssetDocumentSource::DocumentPathSpec& spec, const fs::path& repositoryRoot, bool preferCompilerFallback) { XCUIAssetDocumentSource::DocumentLoadState state = {}; state.kind = spec.kind; state.expectedRootTag = spec.expectedRootTag; state.primaryRelativePath = spec.primaryRelativePath; state.legacyRelativePath = spec.legacyRelativePath; state.candidatePaths = XCUIAssetDocumentSource::CollectCandidatePaths( spec, repositoryRoot, GetConfiguredResourceRoot()); if (state.candidatePaths.empty()) { state.errorMessage = "Unable to locate XCUI document source. Expected " + spec.primaryRelativePath + " or legacy mirror " + spec.legacyRelativePath + "."; return state; } state.attemptMessages.reserve(state.candidatePaths.size()); for (const XCUIAssetDocumentSource::ResolutionCandidate& candidate : state.candidatePaths) { state.requestedPath = candidate.requestPath; state.resolvedPath = candidate.resolvedPath; state.pathOrigin = candidate.origin; state.usedLegacyFallback = candidate.origin == XCUIAssetDocumentSource::PathOrigin::LegacyMirror; UIDocumentCompileResult compileResult = {}; if (TryCompileDocumentDirect(spec, candidate.requestPath, compileResult)) { state.backend = XCUIAssetDocumentSource::LoadBackend::CompilerFallback; state.compileResult = std::move(compileResult); state.sourcePath = ToStdString(state.compileResult.document.sourcePath); if (state.sourcePath.empty()) { state.sourcePath = ToGenericString(candidate.resolvedPath); } state.statusMessage = (preferCompilerFallback ? std::string("Tracked source changed; direct compile refresh succeeded from ") : std::string("Direct compile load succeeded from ")) + ToGenericString(candidate.resolvedPath) + "."; state.succeeded = true; return state; } UIDocumentCompileResult resourceResult = {}; if (TryLoadDocumentFromResourceManager(spec.kind, candidate.requestPath, resourceResult)) { state.backend = XCUIAssetDocumentSource::LoadBackend::ResourceManager; state.compileResult = std::move(resourceResult); state.sourcePath = ToStdString(state.compileResult.document.sourcePath); if (state.sourcePath.empty()) { state.sourcePath = ToGenericString(candidate.resolvedPath); } state.statusMessage = std::string("Loaded via ResourceManager from ") + ToString(candidate.origin) + " path: " + ToGenericString(candidate.resolvedPath) + "."; state.succeeded = true; return state; } const std::string compileError = compileResult.errorMessage.Empty() ? std::string("ResourceManager load failed and compile fallback returned no diagnostic.") : ToStdString(compileResult.errorMessage); state.attemptMessages.push_back( std::string(ToString(candidate.origin)) + " candidate " + ToGenericString(candidate.resolvedPath) + " -> " + compileError); } state.requestedPath.clear(); state.resolvedPath.clear(); state.sourcePath.clear(); state.backend = XCUIAssetDocumentSource::LoadBackend::None; state.pathOrigin = XCUIAssetDocumentSource::PathOrigin::None; state.usedLegacyFallback = false; state.errorMessage = state.attemptMessages.empty() ? std::string("Failed to load XCUI document.") : state.attemptMessages.front(); if (state.attemptMessages.size() > 1u) { state.errorMessage += " ("; state.errorMessage += std::to_string( static_cast(state.attemptMessages.size())); state.errorMessage += " candidates tried)"; } return state; } } // namespace const char* ToString(XCUIAssetDocumentSource::PathOrigin origin) { switch (origin) { case XCUIAssetDocumentSource::PathOrigin::ProjectAssets: return "project-assets"; case XCUIAssetDocumentSource::PathOrigin::LegacyMirror: return "legacy-mirror"; default: return "none"; } } const char* ToString(XCUIAssetDocumentSource::LoadBackend backend) { switch (backend) { case XCUIAssetDocumentSource::LoadBackend::ResourceManager: return "resource-manager"; case XCUIAssetDocumentSource::LoadBackend::CompilerFallback: return "compiler-fallback"; default: return "none"; } } XCUIAssetDocumentSource::XCUIAssetDocumentSource() = default; XCUIAssetDocumentSource::XCUIAssetDocumentSource(PathSet paths) : m_paths(std::move(paths)) { } void XCUIAssetDocumentSource::SetPathSet(PathSet paths) { m_paths = std::move(paths); } const XCUIAssetDocumentSource::PathSet& XCUIAssetDocumentSource::GetPathSet() const { return m_paths; } bool XCUIAssetDocumentSource::Reload() { const bool preferCompilerFallback = m_state.succeeded && HasTrackedChanges(); m_state = LoadState(); m_state.paths = m_paths; m_state.repositoryDiscovery = DiagnoseRepositoryRoot(m_paths); m_state.repositoryRoot = m_state.repositoryDiscovery.repositoryRoot; m_state.view = LoadDocument(m_paths.view, m_state.repositoryRoot, preferCompilerFallback); if (!m_state.view.succeeded) { m_state.errorMessage = m_state.view.errorMessage; if (!m_state.repositoryDiscovery.statusMessage.empty()) { m_state.errorMessage += " " + m_state.repositoryDiscovery.statusMessage; } m_state.statusMessage = "XCUI view document load failed. " + m_state.repositoryDiscovery.statusMessage; m_trackedWriteTimes.clear(); return false; } m_state.theme = LoadDocument(m_paths.theme, m_state.repositoryRoot, preferCompilerFallback); if (!m_state.theme.succeeded) { m_state.errorMessage = m_state.theme.errorMessage; if (!m_state.repositoryDiscovery.statusMessage.empty()) { m_state.errorMessage += " " + m_state.repositoryDiscovery.statusMessage; } m_state.statusMessage = "XCUI theme document load failed. " + m_state.repositoryDiscovery.statusMessage; m_trackedWriteTimes.clear(); return false; } std::unordered_set seenPaths = {}; CollectTrackedSourcePaths(m_state.view, m_state.trackedSourcePaths, seenPaths); CollectTrackedSourcePaths(m_state.theme, m_state.trackedSourcePaths, seenPaths); UpdateTrackedWriteTimes( m_state, m_trackedWriteTimes, m_state.missingTrackedSourcePaths, m_state.changeTrackingReady, m_state.trackingStatusMessage); m_state.usedLegacyFallback = m_state.view.usedLegacyFallback || m_state.theme.usedLegacyFallback; m_state.succeeded = true; m_state.statusMessage = (m_state.usedLegacyFallback ? std::string("XCUI documents loaded with legacy mirror fallback. ") : std::string("XCUI documents loaded from Assets/XCUI/NewEditor. ")) + m_state.trackingStatusMessage; return true; } bool XCUIAssetDocumentSource::ReloadIfChanged() { if (!m_state.succeeded) { return Reload(); } return HasTrackedChanges() ? Reload() : true; } bool XCUIAssetDocumentSource::HasTrackedChanges() const { if (!m_state.succeeded) { return true; } if (m_state.trackedSourcePaths.empty()) { return false; } if (m_trackedWriteTimes.size() != m_state.trackedSourcePaths.size()) { return true; } for (const TrackedWriteTime& tracked : m_trackedWriteTimes) { fs::file_time_type currentWriteTime = {}; if (!TryGetWriteTime(tracked.path, currentWriteTime) || currentWriteTime != tracked.writeTime) { return true; } } return false; } bool XCUIAssetDocumentSource::IsLoaded() const { return m_state.succeeded; } const XCUIAssetDocumentSource::LoadState& XCUIAssetDocumentSource::GetState() const { return m_state; } XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakePathSet( const std::string& setName) { PathSet paths = {}; paths.setName = SanitizeSetName(setName); paths.view.kind = UIDocumentKind::View; paths.view.expectedRootTag = "View"; paths.view.primaryRelativePath = BuildProjectAssetViewPath(paths.setName); paths.view.legacyRelativePath = BuildLegacyViewPath(paths.setName); paths.theme.kind = UIDocumentKind::Theme; paths.theme.expectedRootTag = "Theme"; paths.theme.primaryRelativePath = BuildProjectAssetThemePath(paths.setName); paths.theme.legacyRelativePath = BuildLegacyThemePath(paths.setName); return paths; } XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakeDemoPathSet() { return MakePathSet("Demo"); } XCUIAssetDocumentSource::PathSet XCUIAssetDocumentSource::MakeLayoutLabPathSet() { return MakePathSet("LayoutLab"); } std::vector XCUIAssetDocumentSource::CollectCandidatePaths( const DocumentPathSpec& spec, const fs::path& repositoryRoot) { return CollectCandidatePaths(spec, repositoryRoot, GetConfiguredResourceRoot()); } std::vector XCUIAssetDocumentSource::CollectCandidatePaths( const DocumentPathSpec& spec, const fs::path& repositoryRoot, const fs::path& resourceRoot) { std::vector candidates = {}; std::unordered_set seenPaths = {}; AddRelativeCandidateIfReachable( spec.primaryRelativePath, repositoryRoot, resourceRoot, PathOrigin::ProjectAssets, candidates, seenPaths); AddRelativeCandidateIfReachable( spec.legacyRelativePath, repositoryRoot, resourceRoot, PathOrigin::LegacyMirror, candidates, seenPaths); return candidates; } XCUIAssetDocumentSource::RepositoryDiscovery XCUIAssetDocumentSource::DiagnoseRepositoryRoot(const PathSet& paths) { return DiagnoseRepositoryRoot(paths, {}, true); } XCUIAssetDocumentSource::RepositoryDiscovery XCUIAssetDocumentSource::DiagnoseRepositoryRoot( const PathSet& paths, const std::vector& searchRoots, bool includeDefaultSearchRoots) { RepositoryDiscovery discovery = {}; discovery.probes.reserve(searchRoots.size() + 3u); const std::vector effectiveSearchRoots = BuildRepositoryRootSearchRoots( searchRoots, includeDefaultSearchRoots); discovery.probes.reserve(effectiveSearchRoots.size()); for (const fs::path& searchRoot : effectiveSearchRoots) { RepositoryProbe probe = {}; probe.searchRoot = searchRoot; fs::path current = searchRoot; while (!current.empty()) { const std::optional matchedRelativePath = FindKnownDocumentUnderRoot(current, paths); if (matchedRelativePath.has_value()) { probe.matched = true; probe.matchedRoot = current.lexically_normal(); probe.matchedRelativePath = *matchedRelativePath; discovery.repositoryRoot = probe.matchedRoot; discovery.probes.push_back(std::move(probe)); discovery.statusMessage = "Repository root resolved to " + ToGenericString(discovery.repositoryRoot) + " via " + discovery.probes.back().matchedRelativePath + "."; return discovery; } const fs::path parent = current.parent_path(); if (parent == current) { break; } current = parent; } discovery.probes.push_back(std::move(probe)); } discovery.statusMessage = "Repository root not found for XCUI set '" + paths.setName + "'. Probed " + std::to_string(static_cast(discovery.probes.size())) + " search root(s)."; return discovery; } std::string XCUIAssetDocumentSource::BuildProjectAssetViewPath( const std::string& setName) { const std::string folderName = SanitizeSetName(setName); return std::string(kProjectAssetRoot) + "/" + folderName + "/View.xcui"; } std::string XCUIAssetDocumentSource::BuildProjectAssetThemePath( const std::string& setName) { const std::string folderName = SanitizeSetName(setName); return std::string(kProjectAssetRoot) + "/" + folderName + "/Theme.xctheme"; } std::string XCUIAssetDocumentSource::BuildLegacyViewPath( const std::string& setName) { return std::string(kLegacyResourceRoot) + "/xcui_" + ToSnakeCase(SanitizeSetName(setName)) + "_view.xcui"; } std::string XCUIAssetDocumentSource::BuildLegacyThemePath( const std::string& setName) { return std::string(kLegacyResourceRoot) + "/xcui_" + ToSnakeCase(SanitizeSetName(setName)) + "_theme.xctheme"; } } // namespace XCUIBackend } // namespace Editor } // namespace XCEngine