Add XCUI new editor sandbox phase 1
This commit is contained in:
750
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp
Normal file
750
new_editor/src/XCUIBackend/XCUIAssetDocumentSource.cpp
Normal file
@@ -0,0 +1,750 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user