Files
XCEngine/new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp

751 lines
25 KiB
C++

#include "XCUIAssetDocumentSource.h"
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
#include <algorithm>
#include <cctype>
#include <optional>
#include <type_traits>
#include <unordered_set>
#include <utility>
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<char>(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<char>(rawCh);
const bool isUpper = std::isupper(rawCh) != 0;
const bool previousIsLowerOrDigit =
std::islower(static_cast<unsigned char>(previous)) != 0 ||
std::isdigit(static_cast<unsigned char>(previous)) != 0;
if (isUpper && !snake.empty() && previousIsLowerOrDigit && snake.back() != '_') {
snake.push_back('_');
}
snake.push_back(static_cast<char>(std::tolower(rawCh)));
previous = ch;
}
while (!snake.empty() && snake.back() == '_') {
snake.pop_back();
}
return snake.empty() ? std::string("default") : snake;
}
std::optional<fs::path> 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<std::string> 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<fs::path>& outRoots,
std::unordered_set<std::string>& 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<fs::path> BuildRepositoryRootSearchRoots(
const std::vector<fs::path>& explicitSearchRoots,
bool includeDefaultSearchRoots) {
std::vector<fs::path> searchRoots = {};
std::unordered_set<std::string> 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<fs::path> currentPath = GetCurrentPath();
if (currentPath.has_value()) {
AppendUniqueSearchRoot(*currentPath, searchRoots, seenRoots);
}
return searchRoots;
}
void AddCandidate(
std::vector<XCUIAssetDocumentSource::ResolutionCandidate>& candidates,
std::unordered_set<std::string>& 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<XCUIAssetDocumentSource::ResolutionCandidate>& candidates,
std::unordered_set<std::string>& 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 <typename TDocumentResource>
bool TryLoadDocumentFromResourceManager(
const std::string& requestPath,
UIDocumentCompileResult& outResult) {
static_assert(
std::is_base_of_v<UIDocumentResource, TDocumentResource>,
"TDocumentResource must derive from UIDocumentResource");
auto resource = ResourceManager::Get().Load<TDocumentResource>(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<UIView>(requestPath, outResult);
case UIDocumentKind::Theme:
return TryLoadDocumentFromResourceManager<UITheme>(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<std::string>& outPaths,
std::unordered_set<std::string>& 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<XCUIAssetDocumentSource::TrackedWriteTime>& outTrackedWriteTimes,
std::vector<std::string>& 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<unsigned long long>(outTrackedWriteTimes.size())) +
" XCUI source file(s) for hot reload.";
return;
}
outTrackingStatusMessage =
"Tracking " +
std::to_string(static_cast<unsigned long long>(outTrackedWriteTimes.size())) +
" of " +
std::to_string(static_cast<unsigned long long>(state.trackedSourcePaths.size())) +
" XCUI source file(s); unable to stat " +
std::to_string(static_cast<unsigned long long>(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<unsigned long long>(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<std::string> 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::ResolutionCandidate>
XCUIAssetDocumentSource::CollectCandidatePaths(
const DocumentPathSpec& spec,
const fs::path& repositoryRoot) {
return CollectCandidatePaths(spec, repositoryRoot, GetConfiguredResourceRoot());
}
std::vector<XCUIAssetDocumentSource::ResolutionCandidate>
XCUIAssetDocumentSource::CollectCandidatePaths(
const DocumentPathSpec& spec,
const fs::path& repositoryRoot,
const fs::path& resourceRoot) {
std::vector<ResolutionCandidate> candidates = {};
std::unordered_set<std::string> 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<fs::path>& searchRoots,
bool includeDefaultSearchRoots) {
RepositoryDiscovery discovery = {};
discovery.probes.reserve(searchRoots.size() + 3u);
const std::vector<fs::path> 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<std::string> 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<unsigned long long>(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