Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da40dd474 | ||
|
|
8a82c0da1f | ||
|
|
c82eca1eca | ||
|
|
206f3b9694 | ||
|
|
28b8b5aad7 | ||
|
|
be845878b4 | ||
|
|
bd2b51ea91 | ||
|
|
184c1307d1 | ||
|
|
2bd0e61520 | ||
|
|
9f77923068 | ||
|
|
3005fccf23 | ||
|
|
1cd6e7d970 | ||
|
|
45605001f6 | ||
|
|
35b11eea34 | ||
|
|
d6aabb5dec | ||
|
|
bb7edfbf01 | ||
|
|
b9f4c59f6d | ||
|
|
3a5fd8e648 | ||
|
|
5812501226 | ||
|
|
7dbf7ea8ec | ||
|
|
65f73025cd | ||
|
|
7cf1d3cbaa | ||
|
|
708af836f5 | ||
|
|
04129a75f3 | ||
|
|
f69897bbb8 | ||
|
|
b09d9d5063 | ||
|
|
2507b9959f | ||
|
|
f0795aa5c2 | ||
|
|
ad4fce6280 | ||
|
|
7122f91671 | ||
|
|
727d2e0b3f | ||
|
|
1d8a0dd8b6 | ||
|
|
698126e1a0 | ||
|
|
e859d65de5 | ||
|
|
dbf0661ce3 | ||
|
|
fdfe43f4b2 | ||
|
|
e92e776a1d | ||
|
|
edd6a603ed | ||
|
|
4dacb6f430 | ||
|
|
6595a8ad1b | ||
|
|
bcc15ee225 | ||
|
|
d6dd2b0920 | ||
|
|
5bd2c0bec8 | ||
|
|
34cdca132e | ||
|
|
c455b7ce9d | ||
|
|
deaec47a12 | ||
|
|
31d2d73744 | ||
|
|
c26ee6d263 | ||
|
|
59ccfe0e3a | ||
|
|
a5ffd10a0d | ||
|
|
10c870b464 | ||
|
|
0de2dfa1ed | ||
|
|
75d4c39bd7 | ||
|
|
4c789e6cfa | ||
|
|
920b718f62 | ||
|
|
0bc599dade | ||
|
|
e7ceff11af | ||
|
|
1f05d971e2 | ||
|
|
5dece0542c | ||
|
|
51797790bd | ||
|
|
31695dcc49 | ||
|
|
51b6b5a123 | ||
|
|
0960931cd4 | ||
|
|
436d8ee068 | ||
|
|
4b782a71fc | ||
|
|
66a1f1690f | ||
|
|
911f63e63c | ||
|
|
46b4f3c0d0 | ||
|
|
f23615ffeb | ||
|
|
fdf756e6a4 | ||
|
|
7aa007b077 | ||
|
|
0c53c21030 | ||
|
|
4dd0f60c75 | ||
|
|
60f714853f | ||
|
|
5511755a5e | ||
|
|
51846f5a2b | ||
|
|
436ea4b29a | ||
|
|
46c8783c72 |
@@ -70,3 +70,23 @@ harness/release-native-child-continuation-review-20260911.md -text -whitespace
|
||||
|
||||
harness/experiments/packaged-gui-close-865483b-20260911/** -text -whitespace
|
||||
harness/release-grapheme-shortening-review-20260911.md -text -whitespace
|
||||
harness/experiments/staged-source-native-a57c334-20260911/** -text -whitespace
|
||||
harness/experiments/grapheme-windows-cb91525-20260911/** -text -whitespace
|
||||
harness/experiments/canvas-native-close-a57c334-20260911/** -text -whitespace
|
||||
harness/release-staged-native-a57-independent-review-20260911.md -text -whitespace
|
||||
harness/release-saved-destination-independent-review-20260912.md -text -whitespace
|
||||
harness/experiments/repair-union-windows-33e30c1-20260912/** -text -whitespace
|
||||
harness/experiments/canvas-camera-a57c334-20260912/** -text -whitespace
|
||||
harness/release-camera-a57-independent-review-20260912.md -text -whitespace
|
||||
|
||||
harness/experiments/canvas-repair-package-33e30c1-20260912/** -text -whitespace
|
||||
harness/experiments/canvas-repair-native-33e30c1-20260912/** -text -whitespace
|
||||
harness/experiments/canvas-repair-server-33e30c1-20260912/** -text -whitespace
|
||||
|
||||
harness/experiments/canvas-repair-publication-20260912/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/attempts-8dd/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/state-picker-a9cd/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/final04-tests/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/final04-complete/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/package-server/** -text -whitespace
|
||||
harness/prerelease37-windows-gates-20260924/native-gui/** -text -whitespace
|
||||
|
||||
@@ -35,20 +35,28 @@ Use Gitea for live coordination and the relevant code version for implementation
|
||||
|
||||
## Coordination and integration
|
||||
|
||||
All independent agents coordinating through Gitea run on completely separate machines, each with its own clone, filesystem, tools, processes, and runtime. Gitea and pushed Git history are the shared communication and delivery channels. Local files (including `harness`), unpushed commits, worktrees, test results, and localhost services are not accessible to peers. Matching branch names or paths do not imply synchronized contents.
|
||||
|
||||
1. On starting or resuming, including after compaction, inspect the working branch, local changes, applicable instructions, relevant shared records, and existing implementation. Recover context from authoritative records. Check for newer shared instructions on the actual remote default branch without overwriting local work. Read what is relevant; avoid repeated full-history scans and constant polling.
|
||||
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, branch/worktree, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
|
||||
3. Independent sessions are peers. Each owns its contribution and delegated work. Record one integration/release owner when a milestone needs shared coordination. Use separate worktrees or checkouts, partition files/interfaces, and coordinate changes to shared specifications and documents.
|
||||
4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and uncommitted or unpublished work; make required artifacts accessible to other sessions. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
|
||||
5. Follow project review and merge conventions. Inspect upstream changes, resolve conflicts while preserving others' edits, and verify the combined result. Do not overwrite others' work or force-push shared history to resolve coordination problems.
|
||||
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository/branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
|
||||
3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration/release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files/interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines.
|
||||
4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
|
||||
5. Follow project review and merge conventions. Fetch the relevant remote branches and inspect the identified commits before reviewing or integrating another machine's work. Resolve conflicts while preserving others' edits, and verify the combined result locally; report material environment differences. Local instruction-file edits reach peers only after publication and retrieval. Do not overwrite others' work or force-push shared history to resolve coordination problems.
|
||||
6. Close work when its acceptance criteria are met and the result is integrated or delivered as appropriate. Mark duplicates and superseded tasks explicitly with replacement links. Then choose further useful unowned work while the requested objective remains incomplete.
|
||||
|
||||
Use Gitea MCP for shared coordination. If unavailable, report the failure, buffer necessary handoff information locally, and continue already-owned or clearly non-overlapping work. Reconcile with current Gitea state when access returns; do not claim an ownership update, review, or publication succeeded without confirmation.
|
||||
|
||||
## Delegation and reasoning
|
||||
|
||||
Use GPT-6 Astra exclusively. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
|
||||
Use GPT-6 Astra exclusively. Gitea peers are independent remote sessions, not subagents controlled by this session. Spawned subagents use the execution environment actually provided by the agent tools; do not assume they share files, and publish their results before Gitea peers need them. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
|
||||
|
||||
Use the highest available reasoning effort for orchestration and foundational semantic, methodological, formal, or cross-domain decisions. For subagents, use extra-high for difficult design, adversarial review, or unusually hard implementation; high for normal implementation, targeted research, correctness review, debugging, and runtime evaluation; medium for routine exploration, mechanical changes, builds, and tests; and lower for trivial work. Allocate by difficulty, not length, using supported controls; do not claim an unapplied change or alter global configuration merely to enforce these preferences.
|
||||
Optimize reasoning effort for verified outcomes per cost, including review, retries, and repair:
|
||||
|
||||
- Main agent: default to high for planning, synthesis, integration, and acceptance decisions; medium for dispatching a settled plan and routine coordination.
|
||||
- Subagents: default to medium for bounded implementation and targeted research; low for straightforward retrieval, mechanical edits, and running established checks; high for complex debugging, correctness review, adversarial analysis, or ambiguous design.
|
||||
- Use extra-high selectively for difficult foundational semantic, methodological, formal, or cross-domain decisions. Reserve max for exceptional unresolved problems where additional reasoning is likely to justify its cost. Retain higher settings when evidence supports their value.
|
||||
|
||||
Allocate by task difficulty and consequences, not role or length alone. Start higher when a clearly difficult decision has expensive downstream consequences. Escalate for concrete reasoning difficulties; resolve missing evidence or unclear requirements at their source. Set subagent effort explicitly through supported controls to avoid unintended inheritance. Do not claim an unapplied change or alter global configuration merely to enforce these preferences.
|
||||
|
||||
## Memory and maintenance
|
||||
|
||||
|
||||
+160
-27
@@ -142,12 +142,87 @@ pub struct Analysis {
|
||||
pub declarations: Vec<DeclarationSpan>,
|
||||
}
|
||||
|
||||
/// Check the decoded spelling required by declaration-header UUID clauses.
|
||||
/// This is the shared 36-byte/hyphen shape rule, not a hexadecimal parser:
|
||||
/// callers must additionally parse the UUID. Uppercase hex and JSON escapes
|
||||
/// that decode to this spelling remain accepted. URN/braced/compact forms do not.
|
||||
pub fn has_uuid_header_shape(decoded: &str) -> bool {
|
||||
decoded.len() == 36
|
||||
&& [8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|index| decoded.as_bytes()[index] == b'-')
|
||||
}
|
||||
|
||||
/// Budgets for one original source buffer. Policy is caller-owned; these
|
||||
/// limits do not change the grammar or perform model admission.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutlineLimits {
|
||||
pub max_source_bytes: usize,
|
||||
pub max_tokens: usize,
|
||||
pub max_declarations: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OutlineResource {
|
||||
SourceBytes,
|
||||
Tokens,
|
||||
Declarations,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutlineLimit {
|
||||
pub resource: OutlineResource,
|
||||
pub limit: usize,
|
||||
pub observed_at_least: usize,
|
||||
/// Original byte offset where admission stopped; never a rewritten offset.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// Roles discovered by the existing tolerant declaration walk. Keeping the
|
||||
/// wrapper preserves DeclarationSpan and Analysis as existing public APIs.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutlineDeclaration {
|
||||
pub declaration: DeclarationSpan,
|
||||
pub kind_span: Span,
|
||||
pub alias_span: Span,
|
||||
/// Whole quoted header ID token, even if it is not a valid UUID. In that
|
||||
/// case declaration.id is None. Missing/non-string ID values have no span.
|
||||
pub id_literal_span: Option<Span>,
|
||||
}
|
||||
|
||||
/// A full-file tolerant scan, not a successful parse or validated model. Invalid
|
||||
/// text remains available. Lexical errors and unmatched braces are facts only:
|
||||
/// their absence does not establish syntactic or semantic validity.
|
||||
///
|
||||
/// A budget failure returns Err and no partial Outline. Include reachability,
|
||||
/// project completeness and editor revision are intentionally not inferred here.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Outline<'a> {
|
||||
pub source: &'a str,
|
||||
pub tokens: Vec<TokenSpan>,
|
||||
pub declarations: Vec<OutlineDeclaration>,
|
||||
pub has_lexical_errors: bool,
|
||||
pub has_unmatched_braces: bool,
|
||||
}
|
||||
|
||||
/// Tolerant highlighting tokens cover the whole input, including incomplete
|
||||
/// strings and unexpected characters. No model is created or modified.
|
||||
pub fn tokenize(source: &str) -> Vec<TokenSpan> {
|
||||
tokenize_with_limit(source, usize::MAX).expect("unlimited token scan")
|
||||
}
|
||||
|
||||
fn tokenize_with_limit(source: &str, max_tokens: usize) -> Result<Vec<TokenSpan>, OutlineLimit> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut cursor = 0;
|
||||
while cursor < source.len() {
|
||||
if tokens.len() == max_tokens {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::Tokens,
|
||||
limit: max_tokens,
|
||||
observed_at_least: tokens.len().saturating_add(1),
|
||||
offset: cursor,
|
||||
});
|
||||
}
|
||||
let start = cursor;
|
||||
let ch = source[cursor..].chars().next().expect("in bounds");
|
||||
let class = if matches!(ch, ' ' | '\t' | '\r' | '\n') {
|
||||
@@ -224,7 +299,7 @@ pub fn tokenize(source: &str) -> Vec<TokenSpan> {
|
||||
span: Span { start, end: cursor },
|
||||
});
|
||||
}
|
||||
tokens
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
pub fn lint(source: &str) -> Vec<Diagnostic> {
|
||||
@@ -1230,11 +1305,7 @@ impl Parser<'_> {
|
||||
self.cursor += 1;
|
||||
let offset = self.current().span.start;
|
||||
let value = self.quoted()?;
|
||||
if value.len() != 36
|
||||
|| ![8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|i| value.as_bytes()[i] == b'-')
|
||||
{
|
||||
if !has_uuid_header_shape(&value) {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
@@ -1962,8 +2033,49 @@ fn take_required_id(fields: &mut Map<String, Value>, key: &str) -> Result<Id, St
|
||||
|
||||
/// Analyze an incomplete source buffer without adopting a candidate. Declaration
|
||||
/// spans remain available even when a later declaration contains errors.
|
||||
/// Invalid compact/braced/URN header IDs retain no authored identity, matching
|
||||
/// the declaration parser rather than the UUID crate's broader accepted forms.
|
||||
pub fn analyze(source: &str) -> Analysis {
|
||||
let tokens = tokenize(source);
|
||||
let (declarations, _) =
|
||||
declaration_outline(source, &tokens, usize::MAX).expect("unlimited declaration walk");
|
||||
Analysis {
|
||||
tokens,
|
||||
diagnostics: lint(source),
|
||||
declarations: declarations.into_iter().map(|d| d.declaration).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read exact current text without lint, parsing a model, stabilization or I/O.
|
||||
/// Source bytes are admitted before token allocation; token/declaration limits
|
||||
/// are checked before lexing the next token or materializing the next record.
|
||||
pub fn outline(source: &str, limits: OutlineLimits) -> Result<Outline<'_>, OutlineLimit> {
|
||||
if source.len() > limits.max_source_bytes {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::SourceBytes,
|
||||
limit: limits.max_source_bytes,
|
||||
observed_at_least: source.len(),
|
||||
offset: 0,
|
||||
});
|
||||
}
|
||||
let tokens = tokenize_with_limit(source, limits.max_tokens)?;
|
||||
let has_lexical_errors = tokens.iter().any(|token| token.class == TokenClass::Error);
|
||||
let (declarations, has_unmatched_braces) =
|
||||
declaration_outline(source, &tokens, limits.max_declarations)?;
|
||||
Ok(Outline {
|
||||
source,
|
||||
tokens,
|
||||
declarations,
|
||||
has_lexical_errors,
|
||||
has_unmatched_braces,
|
||||
})
|
||||
}
|
||||
|
||||
fn declaration_outline(
|
||||
source: &str,
|
||||
tokens: &[TokenSpan],
|
||||
max_declarations: usize,
|
||||
) -> Result<(Vec<OutlineDeclaration>, bool), OutlineLimit> {
|
||||
let significant = tokens
|
||||
.iter()
|
||||
.filter(|t| !matches!(t.class, TokenClass::Whitespace | TokenClass::Comment))
|
||||
@@ -1972,17 +2084,21 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
// again would make tolerant navigation quadratic on deeply nested drafts.
|
||||
let mut body_ends = vec![None; significant.len()];
|
||||
let mut bodies = Vec::new();
|
||||
let mut unmatched_close = false;
|
||||
for (index, token) in significant.iter().enumerate() {
|
||||
match &source[token.span.start..token.span.end] {
|
||||
"{" => bodies.push(index),
|
||||
"}" => {
|
||||
if let Some(open) = bodies.pop() {
|
||||
body_ends[open] = Some(token.span.end);
|
||||
} else {
|
||||
unmatched_close = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let has_unmatched_braces = unmatched_close || !bodies.is_empty();
|
||||
let mut declarations = Vec::new();
|
||||
for (i, t) in significant.iter().enumerate() {
|
||||
let word = &source[t.span.start..t.span.end];
|
||||
@@ -2007,7 +2123,16 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
if declarations.len() == max_declarations {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::Declarations,
|
||||
limit: max_declarations,
|
||||
observed_at_least: declarations.len().saturating_add(1),
|
||||
offset: t.span.start,
|
||||
});
|
||||
}
|
||||
let mut id = None;
|
||||
let mut id_literal_span = None;
|
||||
if significant
|
||||
.get(j)
|
||||
.is_some_and(|t| &source[t.span.start..t.span.end] == "id")
|
||||
@@ -2015,39 +2140,51 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
if let Some(value) = significant.get(j + 1)
|
||||
&& value.class == TokenClass::String
|
||||
{
|
||||
id_literal_span = Some(value.span);
|
||||
let text =
|
||||
serde_json::from_str::<String>(&source[value.span.start..value.span.end])
|
||||
.unwrap();
|
||||
id = Id::parse_str(&text).ok();
|
||||
if has_uuid_header_shape(&text) {
|
||||
id = Id::parse_str(&text).ok();
|
||||
}
|
||||
}
|
||||
j += 2;
|
||||
}
|
||||
let mut end = significant
|
||||
.get(j)
|
||||
.map_or(alias_token.span.end, |t| t.span.end);
|
||||
// A recognized header ID at EOF must remain inside the whole span.
|
||||
// Keep the old next-token/body behavior; only the EOF fallback changes.
|
||||
let header_end = significant
|
||||
.get(j.saturating_sub(1))
|
||||
.or_else(|| significant.last())
|
||||
.map_or(alias_token.span.end, |token| token.span.end);
|
||||
let mut end = significant.get(j).map_or(header_end, |t| t.span.end);
|
||||
if significant
|
||||
.get(j)
|
||||
.is_some_and(|t| &source[t.span.start..t.span.end] == "{")
|
||||
{
|
||||
end = body_ends[j].unwrap_or(source.len());
|
||||
}
|
||||
declarations.push(DeclarationSpan {
|
||||
alias: alias.into(),
|
||||
id,
|
||||
kind: word.into(),
|
||||
span: Span {
|
||||
start: t.span.start,
|
||||
end,
|
||||
declarations.push(OutlineDeclaration {
|
||||
declaration: DeclarationSpan {
|
||||
alias: alias.into(),
|
||||
id,
|
||||
kind: word.into(),
|
||||
span: Span {
|
||||
start: t.span.start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
kind_span: t.span,
|
||||
alias_span: alias_token.span,
|
||||
id_literal_span,
|
||||
});
|
||||
}
|
||||
Analysis {
|
||||
tokens,
|
||||
diagnostics: lint(source),
|
||||
declarations,
|
||||
}
|
||||
Ok((declarations, has_unmatched_braces))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_outline_tests.rs"]
|
||||
mod outline_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_nesting_tests.rs"]
|
||||
mod nesting_tests;
|
||||
@@ -2442,11 +2579,7 @@ pub(crate) mod legacy {
|
||||
let value = self.quoted()?;
|
||||
// Require the portable 8-4-4-4-12 spelling, not the UUID crate's URN,
|
||||
// braces, or compact alternatives. Uppercase hexadecimal is accepted.
|
||||
if value.len() != 36
|
||||
|| ![8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|index| value.as_bytes()[index] == b'-')
|
||||
{
|
||||
if !super::has_uuid_header_shape(&value) {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
//! Prepared outline tests. Explicit compatibility tests additionally exercise
|
||||
//! the existing parsers/analyze; the outline entry itself never calls them.
|
||||
use super::*;
|
||||
|
||||
fn limits() -> OutlineLimits {
|
||||
OutlineLimits {
|
||||
max_source_bytes: 1024 * 1024,
|
||||
max_tokens: 100_000,
|
||||
max_declarations: 1_000,
|
||||
}
|
||||
}
|
||||
|
||||
fn declaration<'a>(result: &'a Outline<'_>, alias: &str) -> &'a OutlineDeclaration {
|
||||
result
|
||||
.declarations
|
||||
.iter()
|
||||
.find(|d| d.declaration.alias == alias)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn text(source: &str, span: Span) -> &str {
|
||||
&source[span.start..span.end]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_roles_preserve_unicode_crlf_and_exact_escaped_uuid_literal() {
|
||||
let source = "// 検査 🚒\r\noa_actor responder \"救助\" id \"\\u00300000000-0000-0000-0000-000000000001\" {}\r\nsource @ // 使用\r\n responder;";
|
||||
let before = source.as_bytes().to_vec();
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.source.as_bytes(), before);
|
||||
assert_eq!(result.source.as_ptr(), source.as_ptr());
|
||||
let d = declaration(&result, "responder");
|
||||
assert_eq!(text(source, d.kind_span), "oa_actor");
|
||||
assert_eq!(text(source, d.alias_span), "responder");
|
||||
assert_eq!(
|
||||
text(source, d.id_literal_span.unwrap()),
|
||||
"\"\\u00300000000-0000-0000-0000-000000000001\""
|
||||
);
|
||||
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
||||
assert_eq!(d.declaration.span.start, source.find("oa_actor").unwrap());
|
||||
assert_eq!(d.declaration.span.end, source.find("{}\r\n").unwrap() + 2);
|
||||
let mut end = 0;
|
||||
for token in &result.tokens {
|
||||
assert_eq!(token.span.start, end);
|
||||
assert!(token.span.start < token.span.end);
|
||||
assert!(source.is_char_boundary(token.span.start));
|
||||
assert!(source.is_char_boundary(token.span.end));
|
||||
end = token.span.end;
|
||||
}
|
||||
assert_eq!(end, source.len());
|
||||
assert!(!result.has_lexical_errors);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_id_at_eof_is_inside_the_declaration_extent() {
|
||||
for source in [
|
||||
"oa_actor a \"Actor\" id \"00000000-0000-0000-0000-000000000001\"",
|
||||
"node placed id \"00000000-0000-0000-0000-000000000001\"",
|
||||
] {
|
||||
let result = outline(source, limits()).unwrap();
|
||||
let d = &result.declarations[0];
|
||||
let literal = d.id_literal_span.unwrap();
|
||||
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
||||
assert_eq!(d.declaration.span.end, source.len());
|
||||
assert!(d.declaration.span.start <= literal.start);
|
||||
assert!(literal.end <= d.declaration.span.end);
|
||||
}
|
||||
for source in ["oa_actor a \"Actor\"", "oa_actor a \"Actor\" id"] {
|
||||
let result = outline(source, limits()).unwrap();
|
||||
let d = &result.declarations[0];
|
||||
assert_eq!(d.declaration.span.end, source.len());
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(d.id_literal_span, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_bad_and_historical_ids_never_allocate_authored_identity() {
|
||||
let source = "oa_actor missing \"No ID\" {}\noa_actor bad \"Bad ID\" id \"not-a-uuid\" {}\noa_actor historical \"00000000-0000-0000-0000-000000000001\" { history uuid \"00000000-0000-0000-0000-000000000001\"; }";
|
||||
let first = outline(source, limits()).unwrap();
|
||||
let second = outline(source, limits()).unwrap();
|
||||
assert_eq!(first, second);
|
||||
assert!(
|
||||
first
|
||||
.declarations
|
||||
.iter()
|
||||
.all(|d| d.declaration.id.is_none())
|
||||
);
|
||||
assert_eq!(declaration(&first, "missing").id_literal_span, None);
|
||||
assert_eq!(declaration(&first, "historical").id_literal_span, None);
|
||||
assert_eq!(
|
||||
text(source, declaration(&first, "bad").id_literal_span.unwrap()),
|
||||
"\"not-a-uuid\""
|
||||
);
|
||||
// Balanced tokens are not a successful syntax/semantic validation claim.
|
||||
assert!(!first.has_lexical_errors);
|
||||
assert!(!first.has_unmatched_braces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_full_scan_keeps_closed_children_and_reports_lexical_body_facts() {
|
||||
let source = "oa_entity outer \"Outer\" {\n oa_actor ready \"Ready\" { description \"} stays text\"; }\n oa_actor pending \"Pending\" { description \"unfinished 🌊";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.source, source);
|
||||
assert_eq!(result.tokens.last().unwrap().span.end, source.len());
|
||||
assert!(result.has_lexical_errors);
|
||||
assert!(result.has_unmatched_braces);
|
||||
let ready = declaration(&result, "ready");
|
||||
assert_eq!(
|
||||
text(source, ready.declaration.span),
|
||||
"oa_actor ready \"Ready\" { description \"} stays text\"; }"
|
||||
);
|
||||
assert_eq!(
|
||||
declaration(&result, "outer").declaration.span.end,
|
||||
source.len()
|
||||
);
|
||||
assert_eq!(
|
||||
declaration(&result, "pending").declaration.span.end,
|
||||
source.len()
|
||||
);
|
||||
let unmatched_close = outline("}\n// brace in comment {\n", limits()).unwrap();
|
||||
assert!(unmatched_close.has_unmatched_braces);
|
||||
assert!(!unmatched_close.has_lexical_errors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_body_syntax_is_unchecked_even_when_known_error_flags_are_clear() {
|
||||
let source = "oa_actor a \"A\" id \"00000000-0000-0000-0000-000000000001\"";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 1);
|
||||
assert!(!result.has_lexical_errors);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
// There is deliberately no `valid` flag: the known-complete fact is scanning
|
||||
// these bytes, not the missing body becoming legal or the draft being applied.
|
||||
assert_eq!(result.source, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyword_alias_and_collisions_keep_every_source_ordered_header() {
|
||||
let source = "project root_model \"Model\" {\n oa_actor oa_activity \"First\" id \"00000000-0000-0000-0000-000000000001\" {}\n oa_actor oa_activity \"Second\" id \"00000000-0000-0000-0000-000000000001\" {}\n diagram view_one \"View\" {}\n node placement { diagram @view_one; }\n}";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 5);
|
||||
let duplicates: Vec<_> = result
|
||||
.declarations
|
||||
.iter()
|
||||
.filter(|d| d.declaration.alias == "oa_activity")
|
||||
.collect();
|
||||
assert_eq!(duplicates.len(), 2);
|
||||
assert!(duplicates[0].alias_span.start < duplicates[1].alias_span.start);
|
||||
assert_eq!(duplicates[0].declaration.id, duplicates[1].declaration.id);
|
||||
assert!(
|
||||
result
|
||||
.tokens
|
||||
.iter()
|
||||
.any(|t| t.span == duplicates[0].alias_span && t.class == TokenClass::Keyword)
|
||||
);
|
||||
assert_eq!(
|
||||
text(source, declaration(&result, "placement").kind_span),
|
||||
"node"
|
||||
);
|
||||
assert_eq!(declaration(&result, "placement").declaration.id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_refuse_before_returning_a_partial_outline() {
|
||||
let source = "oa_actor a \"A\" {}\noa_actor b \"B\" {}";
|
||||
let admitted = outline(source, limits()).unwrap();
|
||||
let exact = OutlineLimits {
|
||||
max_source_bytes: source.len(),
|
||||
max_tokens: admitted.tokens.len(),
|
||||
max_declarations: 2,
|
||||
};
|
||||
assert_eq!(outline(source, exact).unwrap(), admitted);
|
||||
for resource in [
|
||||
OutlineResource::SourceBytes,
|
||||
OutlineResource::Tokens,
|
||||
OutlineResource::Declarations,
|
||||
] {
|
||||
let mut lower = exact;
|
||||
match resource {
|
||||
OutlineResource::SourceBytes => lower.max_source_bytes -= 1,
|
||||
OutlineResource::Tokens => lower.max_tokens -= 1,
|
||||
OutlineResource::Declarations => lower.max_declarations -= 1,
|
||||
}
|
||||
let error = outline(source, lower).unwrap_err();
|
||||
assert_eq!(error.resource, resource);
|
||||
assert!(error.observed_at_least > error.limit);
|
||||
assert!(source.is_char_boundary(error.offset));
|
||||
if resource == OutlineResource::Declarations {
|
||||
assert_eq!(error.offset, source.find("oa_actor b").unwrap());
|
||||
}
|
||||
}
|
||||
let empty = outline(
|
||||
"",
|
||||
OutlineLimits {
|
||||
max_source_bytes: 0,
|
||||
max_tokens: 0,
|
||||
max_declarations: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(empty.tokens.is_empty() && empty.declarations.is_empty());
|
||||
let unicode = "// 🌊\r\noa_actor a \"A\" {}";
|
||||
let error = outline(
|
||||
unicode,
|
||||
OutlineLimits {
|
||||
max_source_bytes: 4,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::SourceBytes);
|
||||
assert_eq!(error.offset, 0);
|
||||
let error = outline(
|
||||
unicode,
|
||||
OutlineLimits {
|
||||
max_tokens: 1,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::Tokens);
|
||||
assert_eq!(error.offset, unicode.find('\n').unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_ranges_follow_current_bytes_without_identity_stabilization() {
|
||||
let old = "oa_actor a \"A\" {}\nsource @a;";
|
||||
let current = format!("// 航海 🚒\r\n{old}");
|
||||
let old_outline = outline(old, limits()).unwrap();
|
||||
let current_outline = outline(¤t, limits()).unwrap();
|
||||
let shift = current.len() - old.len();
|
||||
let old_role = &old_outline.declarations[0];
|
||||
let current_role = ¤t_outline.declarations[0];
|
||||
assert_eq!(
|
||||
current_role.alias_span.start,
|
||||
old_role.alias_span.start + shift
|
||||
);
|
||||
assert_eq!(
|
||||
current_role.kind_span.start,
|
||||
old_role.kind_span.start + shift
|
||||
);
|
||||
assert_eq!(
|
||||
current_role.declaration.span.end,
|
||||
old_role.declaration.span.end + shift
|
||||
);
|
||||
assert_eq!(current_outline.source, current);
|
||||
assert_eq!(old_outline.source, old);
|
||||
assert_eq!(current_role.declaration.id, None);
|
||||
assert!(!current_outline.source.contains(" id "));
|
||||
// Workspace revision/stale-handle rejection belongs to the query/adapter,
|
||||
// not a claim that an old owned span can be applied to any other string.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_bounds_use_the_same_iterative_walk_and_declaration_budget() {
|
||||
let mut source = String::new();
|
||||
for index in 0..600 {
|
||||
source.push_str(&format!("oa_activity work_{index} \"Work\" {{\n"));
|
||||
}
|
||||
source.push_str(&"}\n".repeat(600));
|
||||
let result = outline(&source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 600);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
assert_eq!(
|
||||
result.declarations[0].declaration.span.end,
|
||||
source.len() - 1
|
||||
);
|
||||
let error = outline(
|
||||
&source,
|
||||
OutlineLimits {
|
||||
max_declarations: 599,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::Declarations);
|
||||
assert_eq!(error.observed_at_least, 600);
|
||||
assert_eq!(error.offset, source.find("oa_activity work_599").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noncanonical_uuid_spellings_have_no_outline_or_analysis_identity() {
|
||||
for raw in [
|
||||
"00000000000000000000000000000001",
|
||||
"{00000000-0000-0000-0000-000000000001}",
|
||||
"urn:uuid:00000000-0000-0000-0000-000000000001",
|
||||
] {
|
||||
assert!(
|
||||
Id::parse_str(raw).is_ok(),
|
||||
"The UUID crate alone is too permissive"
|
||||
);
|
||||
assert!(!has_uuid_header_shape(raw));
|
||||
let literal = format!("\"{raw}\"");
|
||||
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let before = modern.clone();
|
||||
let result = outline(&modern, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
||||
assert_eq!(modern, before);
|
||||
let errors = parse(&modern).unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12")
|
||||
&& error.span.start == modern.find(&literal).unwrap())
|
||||
);
|
||||
// Intentional compatibility correction: old analyze yielded Some(Id(1))
|
||||
// here despite lint rejecting the header. It now shares outline's None.
|
||||
let analysis = analyze(&modern);
|
||||
assert_eq!(
|
||||
analysis
|
||||
.declarations
|
||||
.iter()
|
||||
.find(|d| d.alias == "model")
|
||||
.unwrap()
|
||||
.id,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
analysis
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12"))
|
||||
);
|
||||
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
||||
let legacy_errors = legacy::parse(&legacy_source).unwrap_err();
|
||||
assert!(
|
||||
legacy_errors
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uppercase_and_json_escaped_header_ids_survive_both_parsers_and_outline() {
|
||||
let expected = Id::from_u128(0xabcdef);
|
||||
let canonical = expected.to_string();
|
||||
let uppercase = canonical.to_uppercase();
|
||||
assert_ne!(uppercase, canonical);
|
||||
for raw in [uppercase.clone(), format!("\\u0030{}", &uppercase[1..])] {
|
||||
let literal = format!("\"{raw}\"");
|
||||
let decoded: String = serde_json::from_str(&literal).unwrap();
|
||||
assert!(has_uuid_header_shape(&decoded));
|
||||
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let result = outline(&modern, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, Some(expected));
|
||||
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
||||
assert_eq!(result.source.as_bytes(), modern.as_bytes());
|
||||
assert_eq!(parse(&modern).unwrap().id, expected);
|
||||
let analysis = analyze(&modern);
|
||||
assert_eq!(analysis.declarations[0].id, Some(expected));
|
||||
assert!(
|
||||
!analysis
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|error| error.severity == crate::validation::Severity::Error)
|
||||
);
|
||||
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
||||
assert_eq!(legacy::parse(&legacy_source).unwrap().id, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_shape_still_requires_valid_hexadecimal_uuid_data() {
|
||||
let raw = "00000000-0000-0000-0000-00000000000Z";
|
||||
assert!(has_uuid_header_shape(raw));
|
||||
assert!(Id::parse_str(raw).is_err());
|
||||
let literal = format!("\"{raw}\"");
|
||||
let source = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let result = outline(&source, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(text(&source, d.id_literal_span.unwrap()), literal);
|
||||
assert!(
|
||||
parse(&source)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.any(|error| error.message == "Invalid UUID identity.")
|
||||
);
|
||||
assert_eq!(analyze(&source).declarations[0].id, None);
|
||||
}
|
||||
+245
-26
@@ -2,12 +2,15 @@ pub(crate) mod architecture_authoring;
|
||||
#[cfg(test)]
|
||||
mod architecture_authoring_tests;
|
||||
mod checkpoints;
|
||||
mod diagram_reveal;
|
||||
#[cfg(test)]
|
||||
mod diagram_selection_tests;
|
||||
mod diagram_usages;
|
||||
mod hosted;
|
||||
mod reference_authoring;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
mod source_symbols;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
@@ -427,9 +430,18 @@ pub enum Message {
|
||||
Linted(u64, Result<Vec<SourceDiagnostic>, String>),
|
||||
SourceDiagnostic(usize),
|
||||
SourceRelated(SourceLocation),
|
||||
SourceViewport,
|
||||
SourceViewport(Option<Id>),
|
||||
SourceSymbols(
|
||||
crate::source_navigation::SymbolContext,
|
||||
crate::source_navigation::SymbolEvent,
|
||||
),
|
||||
SourceSymbolsReady(crate::source_navigation::Completion),
|
||||
DiagramUsage(crate::diagram_usage::Event),
|
||||
DiagramUsagesReady(crate::diagram_usage::Completion),
|
||||
DiagramRevealViewport(crate::diagram_usage::Context, u64, iced::Size),
|
||||
SourceForSelection,
|
||||
SourceNavigation(crate::source_navigation::Event),
|
||||
SourceFindClose(crate::source_navigation::SymbolContext, Id),
|
||||
Save(bool),
|
||||
SavePath(Option<PathBuf>),
|
||||
Saved(Result<Option<FileSnapshot>, SaveFailure>),
|
||||
@@ -507,6 +519,17 @@ pub struct App {
|
||||
pub source_jump: Option<text_editor::Cursor>,
|
||||
source_select_line: bool,
|
||||
pub source_navigation: crate::source_navigation::State,
|
||||
source_find_instance: Id,
|
||||
pub source_symbols: crate::source_navigation::SymbolState,
|
||||
pub(crate) diagram_usages: Option<crate::diagram_usage::Panel>,
|
||||
pub(crate) diagram_navigation_focus: Option<crate::diagram_usage::Focus>,
|
||||
source_diagram_job: Option<Id>,
|
||||
diagram_lookup_in_flight: Option<Id>,
|
||||
pending_diagram_reveal: Option<(
|
||||
crate::diagram_usage::Context,
|
||||
crate::diagram_usage::Usage,
|
||||
u64,
|
||||
)>,
|
||||
pub model_issues: Vec<validation::Issue>,
|
||||
pub migration: Option<persistence::MigrationReport>,
|
||||
source_version: u64,
|
||||
@@ -615,6 +638,13 @@ impl App {
|
||||
source_jump: None,
|
||||
source_select_line: false,
|
||||
source_navigation: crate::source_navigation::State::default(),
|
||||
source_find_instance: Id::new_v4(),
|
||||
source_symbols: crate::source_navigation::SymbolState::default(),
|
||||
diagram_usages: None,
|
||||
diagram_navigation_focus: None,
|
||||
source_diagram_job: None,
|
||||
diagram_lookup_in_flight: None,
|
||||
pending_diagram_reveal: None,
|
||||
model_issues,
|
||||
migration: None,
|
||||
source_version: 0,
|
||||
@@ -668,6 +698,7 @@ impl App {
|
||||
self.lint_requested = None;
|
||||
}
|
||||
fn show_source_file(&mut self, restore_cursor: bool) {
|
||||
self.source_symbols.reveal = None;
|
||||
self.source_tree.reveal(&self.source_workspace.active);
|
||||
self.source_select_line = false;
|
||||
let source = self.source_workspace.text().to_owned();
|
||||
@@ -682,6 +713,9 @@ impl App {
|
||||
.map(|b| b.cursor)
|
||||
})
|
||||
.flatten();
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
}
|
||||
fn selected_architecture_placement(&self) -> Option<Id> {
|
||||
self.active_diagram
|
||||
@@ -935,6 +969,8 @@ impl App {
|
||||
let canvas_size = self.canvas_size;
|
||||
let source_version = self.source_version + 1;
|
||||
let lint_running = self.lint_running;
|
||||
let navigation_in_flight = self.source_symbols.in_flight;
|
||||
let diagram_lookup_in_flight = self.diagram_lookup_in_flight;
|
||||
let guide = std::mem::take(&mut self.guide);
|
||||
let mut hosted = std::mem::take(&mut self.hosted);
|
||||
hosted.leave_document();
|
||||
@@ -946,6 +982,8 @@ impl App {
|
||||
self.canvas_size = canvas_size;
|
||||
self.source_version = source_version;
|
||||
self.lint_running = lint_running;
|
||||
self.source_symbols.in_flight = navigation_in_flight;
|
||||
self.diagram_lookup_in_flight = diagram_lookup_in_flight;
|
||||
self.guide = guide;
|
||||
self.path = path;
|
||||
self.status = "Project ready".into();
|
||||
@@ -1046,22 +1084,70 @@ impl App {
|
||||
crate::model_menu::popup(self),
|
||||
)
|
||||
.into();
|
||||
crate::behavior::scenario::focus::root(
|
||||
behavior::canvas_workspace::focus_root(
|
||||
crate::hosted::view::overlay(self, self.reference_overlay(base)),
|
||||
behavior::canvas_workspace::Scope::input_owner(self),
|
||||
crate::source_navigation::interaction::observe(
|
||||
crate::behavior::scenario::focus::root(
|
||||
behavior::canvas_workspace::focus_root(
|
||||
crate::hosted::view::overlay(
|
||||
self,
|
||||
self.reference_overlay(crate::diagram_usage::view::overlay(self, base)),
|
||||
),
|
||||
behavior::canvas_workspace::Scope::input_owner(self),
|
||||
),
|
||||
self.scenario_focus_owner(),
|
||||
),
|
||||
self.scenario_focus_owner(),
|
||||
self.source_symbols.raw_interaction.clone(),
|
||||
)
|
||||
}
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
self.diagram_usages
|
||||
.as_ref()
|
||||
.map_or_else(Subscription::none, |panel| {
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
let iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key,
|
||||
modifiers,
|
||||
..
|
||||
}) = event
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if !modifiers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match key {
|
||||
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => Some(-1i8),
|
||||
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => Some(1i8),
|
||||
keyboard::Key::Named(keyboard::key::Named::Enter)
|
||||
if status == iced::event::Status::Ignored =>
|
||||
{
|
||||
Some(0i8)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.with(panel.token)
|
||||
.map(|(token, step)| {
|
||||
Message::DiagramUsage(if step == 0 {
|
||||
crate::diagram_usage::Event::Activate(token)
|
||||
} else {
|
||||
crate::diagram_usage::Event::Move(token, step)
|
||||
})
|
||||
})
|
||||
}),
|
||||
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
architecture_authoring::subscription(self),
|
||||
// Use one captured-Escape bridge: a focused Find input consumes
|
||||
// Escape, while modal priority remains in the ordinary handler.
|
||||
behavior::creation::subscription(
|
||||
self.behavior.creation.has_pending() || self.behavior.source_active(),
|
||||
self.behavior.creation.has_pending()
|
||||
|| self.diagram_usages.is_some()
|
||||
|| self.behavior.source_active()
|
||||
|| (self.page == Page::Text
|
||||
&& (self.source_symbols.open || self.source_navigation.open)
|
||||
&& self.architecture.is_none()),
|
||||
)
|
||||
.map(|()| Message::Escape),
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
@@ -1108,6 +1194,17 @@ impl App {
|
||||
])
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
let usage_viewport = self.diagram_reveal_viewport(&message);
|
||||
let window_resized = matches!(&message, Message::WindowResized(_));
|
||||
// Reject stale measurements before legacy viewport handlers can change
|
||||
// dimensions or invalidate the exact pending destination epoch.
|
||||
if self.pending_diagram_reveal.is_some()
|
||||
&& Self::is_diagram_viewport(&message)
|
||||
&& usage_viewport.is_none()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
self.reconcile_pending_diagram_reveal(&message);
|
||||
if let Message::BehaviorControl(intent) = message {
|
||||
return self.behavior_control(intent);
|
||||
}
|
||||
@@ -1118,6 +1215,19 @@ impl App {
|
||||
);
|
||||
let before = self.camera_boundary();
|
||||
let task = self.update_inner(message);
|
||||
if window_resized && let Some((_, _, epoch)) = &mut self.pending_diagram_reveal {
|
||||
*epoch = self.canvas_epoch;
|
||||
}
|
||||
if let Some(surface) = usage_viewport {
|
||||
self.finish_diagram_reveal(surface);
|
||||
}
|
||||
if self
|
||||
.diagram_navigation_focus
|
||||
.as_ref()
|
||||
.is_some_and(|focus| self.diagram_focus(focus.usage.surface).is_none())
|
||||
{
|
||||
self.diagram_navigation_focus = None;
|
||||
}
|
||||
if !camera && self.camera_boundary() != before {
|
||||
self.camera_epoch = self.camera_epoch.wrapping_add(1);
|
||||
self.camera_boundary_epoch = self.canvas_epoch;
|
||||
@@ -1207,6 +1317,75 @@ impl App {
|
||||
)
|
||||
}
|
||||
fn update_inner(&mut self, message: Message) -> Task<Message> {
|
||||
let message = match message {
|
||||
Message::SourceFindClose(context, instance) => {
|
||||
if context != self.source_symbol_context()
|
||||
|| instance != self.source_find_instance
|
||||
|| !self.source_navigation.open
|
||||
|| !self.source_navigation_available()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Only the checked envelope may enter the legacy Close handler.
|
||||
Message::SourceNavigation(crate::source_navigation::Event::Close)
|
||||
}
|
||||
Message::SourceNavigation(crate::source_navigation::Event::Close) => {
|
||||
return Task::none();
|
||||
}
|
||||
message => message,
|
||||
};
|
||||
if let Message::SourceSymbols(context, _) = &message
|
||||
&& (context.session != self.source_workspace.session()
|
||||
|| context.path != self.source_workspace.active)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Internal results and geometry do not constitute a new user intent.
|
||||
// Process query completion even behind a modal to release its worker slot;
|
||||
// its handler independently refuses navigation outside the live editor.
|
||||
if let Message::SourceSymbolsReady(completion) = message {
|
||||
return self.source_symbols_ready(completion);
|
||||
}
|
||||
if let Message::DiagramUsagesReady(completion) = message {
|
||||
return self.diagram_usages_ready(completion);
|
||||
}
|
||||
if !matches!(
|
||||
&message,
|
||||
Message::CheckpointTick
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(..)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::DiagramRevealViewport(..)
|
||||
) {
|
||||
self.source_interaction();
|
||||
}
|
||||
if let Message::DiagramUsage(event) = message {
|
||||
return self.diagram_usage_event(event);
|
||||
}
|
||||
if let Some(panel) = &self.diagram_usages {
|
||||
if matches!(&message, Message::Request(Pending::Close(_))) {
|
||||
self.diagram_usages = None;
|
||||
return self.update(message);
|
||||
}
|
||||
if matches!(&message, Message::Escape) {
|
||||
return self.diagram_usage_event(crate::diagram_usage::Event::Close(panel.token));
|
||||
}
|
||||
if !matches!(
|
||||
&message,
|
||||
Message::CheckpointTick
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(..)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport(_)
|
||||
) {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if let Some(task) = self.reference_message(&message) {
|
||||
return task;
|
||||
}
|
||||
@@ -1274,7 +1453,7 @@ impl App {
|
||||
| Message::CanvasResized(_)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
| Message::Request(Pending::Close(_))
|
||||
)
|
||||
{
|
||||
@@ -1320,7 +1499,7 @@ impl App {
|
||||
| Message::Exported(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
)
|
||||
{
|
||||
return Task::none();
|
||||
@@ -1358,7 +1537,7 @@ impl App {
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::Saved(_)
|
||||
@@ -1969,6 +2148,8 @@ impl App {
|
||||
return self.update(Message::Add(child));
|
||||
}
|
||||
Message::Escape => {
|
||||
let restore_source_focus = self.source_navigation_available()
|
||||
&& (self.source_navigation.open || self.source_symbols.open);
|
||||
if self.behavior.source_back() {
|
||||
self.status =
|
||||
"Returned to the unchanged parent form; no source was created.".into();
|
||||
@@ -1985,6 +2166,12 @@ impl App {
|
||||
self.status = "Drawing cancelled · Select / move is active".into();
|
||||
}
|
||||
self.source_navigation.open = false;
|
||||
self.source_symbols.open = false;
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
if restore_source_focus {
|
||||
self.restore_source_editor_focus();
|
||||
}
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.selected_bend = None;
|
||||
}
|
||||
@@ -3036,6 +3223,9 @@ impl App {
|
||||
if self.path.is_none()
|
||||
|| self.path.as_ref() == Some(&receipt.target)
|
||||
{
|
||||
if self.path.as_ref() != Some(&receipt.target) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.path = Some(receipt.target);
|
||||
self.document.bind_repair_baseline(disk.reachable_bundle());
|
||||
self.disk_project = Some(disk);
|
||||
@@ -3104,13 +3294,28 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::SourceRelated(location) => self.jump_to_source(location),
|
||||
Message::SourceViewport => {
|
||||
if let Some(cursor) = self.source_jump.take() {
|
||||
self.source_editor.move_to(cursor);
|
||||
if self.source_select_line && cursor.selection.is_none() {
|
||||
self.source_editor.perform(text_editor::Action::SelectLine);
|
||||
}
|
||||
return iced::widget::operation::focus("source-editor");
|
||||
Message::SourceViewport(ticket) => return self.source_viewport(ticket),
|
||||
Message::SourceFindClose(..) => return Task::none(), // update checks and unwraps it
|
||||
Message::SourceSymbols(_, event) => return self.source_symbol_event(event),
|
||||
Message::SourceSymbolsReady(completion) => {
|
||||
return self.source_symbols_ready(completion);
|
||||
}
|
||||
Message::DiagramUsage(event) => return self.diagram_usage_event(event),
|
||||
Message::DiagramUsagesReady(completion) => {
|
||||
return self.diagram_usages_ready(completion);
|
||||
}
|
||||
Message::DiagramRevealViewport(context, epoch, size) => {
|
||||
if self
|
||||
.pending_diagram_reveal
|
||||
.as_ref()
|
||||
.is_some_and(|(c, _, e)| *c == context && *e == epoch)
|
||||
&& self
|
||||
.diagram_reveal_viewport(&Message::DiagramRevealViewport(
|
||||
context, epoch, size,
|
||||
))
|
||||
.is_some()
|
||||
{
|
||||
return self.handle(Message::CanvasResized(size));
|
||||
}
|
||||
}
|
||||
Message::ApplySource => {
|
||||
@@ -3157,6 +3362,7 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
self.page = Page::Text;
|
||||
self.source_find_instance = Id::new_v4();
|
||||
self.source_navigation.open = true;
|
||||
self.source_navigation.refresh(&self.source_editor.text());
|
||||
return iced::widget::operation::focus(if go_to_line {
|
||||
@@ -3165,13 +3371,22 @@ impl App {
|
||||
"source-find"
|
||||
});
|
||||
}
|
||||
Event::Close => self.source_navigation.open = false,
|
||||
Event::Close => {
|
||||
let was_open = self.source_navigation.open;
|
||||
self.source_navigation.open = false;
|
||||
if was_open {
|
||||
self.restore_source_editor_focus();
|
||||
}
|
||||
}
|
||||
Event::Query(query) => {
|
||||
self.source_navigation.query = query;
|
||||
self.source_navigation.refresh(&self.source_editor.text());
|
||||
}
|
||||
Event::Next(backwards) if self.page == Page::Text => {
|
||||
self.source_jump = self.source_navigation.next(backwards);
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
if self.source_jump.is_none() {
|
||||
self.status = "No matching source text.".into();
|
||||
}
|
||||
@@ -3182,8 +3397,7 @@ impl App {
|
||||
.destination(self.source_editor.line_count())
|
||||
{
|
||||
Ok(cursor) => {
|
||||
self.source_jump = Some(cursor);
|
||||
self.source_select_line = true;
|
||||
self.queue_source_reveal(cursor, true);
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
},
|
||||
@@ -3266,6 +3480,9 @@ impl App {
|
||||
}
|
||||
self.sync_source();
|
||||
}
|
||||
if self.path.as_ref() != Some(&receipt.path) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.disk_project = Some(disk);
|
||||
self.path = Some(receipt.path);
|
||||
self.hosted.session = None;
|
||||
@@ -3866,7 +4083,7 @@ impl App {
|
||||
self.path = Some(path);
|
||||
self.document.bind_repair_baseline(disk.reachable_bundle());
|
||||
self.source_workspace = Workspace::new(&disk.bundle, false);
|
||||
self.source_workspace.reachable = disk.reachable.clone();
|
||||
self.source_workspace.set_reachable(disk.reachable.clone());
|
||||
self.show_source_file(false);
|
||||
self.source_diagnostics = error.diagnostics().to_vec();
|
||||
self.source_version += 1;
|
||||
@@ -3918,11 +4135,13 @@ impl App {
|
||||
let column = prefix
|
||||
.rfind('\n')
|
||||
.map_or(offset, |start| offset - start - 1);
|
||||
self.source_jump = Some(text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
});
|
||||
self.source_select_line = true;
|
||||
self.queue_source_reveal(
|
||||
text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
fn create_source_file(&mut self) -> Result<(), String> {
|
||||
|
||||
@@ -70,6 +70,7 @@ fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: element,
|
||||
navigation_focus: None,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
|
||||
@@ -225,6 +225,7 @@ fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
navigation_focus: None,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
@@ -299,6 +300,7 @@ fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: element,
|
||||
navigation_focus: None,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Complete a navigation camera after the destination reports its real viewport.
|
||||
use super::*;
|
||||
use crate::diagram_usage::{Context, Surface};
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
impl App {
|
||||
pub(super) fn is_diagram_viewport(message: &Message) -> bool {
|
||||
matches!(
|
||||
message,
|
||||
Message::CanvasResized(_)
|
||||
| Message::DiagramRevealViewport(..)
|
||||
| Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(..))
|
||||
| Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(..))
|
||||
| Message::StateDiagram(behavior::state_edit::Event::Viewport(..))
|
||||
)
|
||||
}
|
||||
pub(crate) fn architecture_reveal_stamp(&self, diagram: Id) -> Option<(Context, u64)> {
|
||||
self.pending_diagram_reveal
|
||||
.as_ref()
|
||||
.filter(|(_, u, _)| u.surface == Surface::Saved(diagram))
|
||||
.map(|(context, _, epoch)| (*context, *epoch))
|
||||
}
|
||||
|
||||
pub(super) fn diagram_reveal_viewport(&self, message: &Message) -> Option<Surface> {
|
||||
let (context, usage, epoch) = self.pending_diagram_reveal.as_ref()?;
|
||||
if context.document != self.canvas_document
|
||||
|| context.revision != self.document.revision()
|
||||
|| *epoch != self.canvas_epoch
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (surface, size, valid) = match message {
|
||||
Message::DiagramRevealViewport(c, e, size) => (
|
||||
usage.surface,
|
||||
*size,
|
||||
c == context
|
||||
&& e == epoch
|
||||
&& self.page == Page::Diagram
|
||||
&& Some(usage.surface.id()) == self.active_diagram,
|
||||
),
|
||||
Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(c, size)) => (
|
||||
Surface::Process(c.owner),
|
||||
*size,
|
||||
c.document == context.document
|
||||
&& c.revision == context.revision
|
||||
&& c.epoch == *epoch,
|
||||
),
|
||||
Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(c, size)) => (
|
||||
Surface::Scenario(c.scenario),
|
||||
*size,
|
||||
c.document == context.document
|
||||
&& c.revision == context.revision
|
||||
&& c.epoch == *epoch,
|
||||
),
|
||||
Message::StateDiagram(behavior::state_edit::Event::Viewport(c, size)) => (
|
||||
Surface::Saved(c.diagram),
|
||||
*size,
|
||||
c.document == context.document
|
||||
&& c.revision == context.revision
|
||||
&& c.epoch == *epoch,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
(valid
|
||||
&& surface == usage.surface
|
||||
&& size.width.is_finite()
|
||||
&& size.height.is_finite()
|
||||
&& size.width > 0.
|
||||
&& size.height > 0.)
|
||||
.then_some(surface)
|
||||
}
|
||||
|
||||
pub(super) fn reconcile_pending_diagram_reveal(&mut self, message: &Message) {
|
||||
if !matches!(
|
||||
message,
|
||||
Message::CheckpointTick
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(..)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::DiagramRevealViewport(..)
|
||||
| Message::DiagramUsage(_)
|
||||
| Message::DiagramUsagesReady(_)
|
||||
| Message::SourceSymbolsReady(_)
|
||||
| Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(..))
|
||||
| Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(..))
|
||||
| Message::StateDiagram(behavior::state_edit::Event::Viewport(..))
|
||||
) {
|
||||
self.pending_diagram_reveal = None;
|
||||
if !Self::is_navigation_camera(message) {
|
||||
self.diagram_navigation_focus = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_navigation_camera(message: &Message) -> bool {
|
||||
if let Message::BehaviorControl(intent) = message {
|
||||
return Self::is_navigation_camera(&intent.message);
|
||||
}
|
||||
matches!(
|
||||
message,
|
||||
Message::Pan(_)
|
||||
| Message::Zoom(_)
|
||||
| Message::Fit
|
||||
| Message::TopologyCanvas(
|
||||
behavior::topology::CanvasEvent::Camera(..)
|
||||
| behavior::topology::CanvasEvent::Zoom(..)
|
||||
| behavior::topology::CanvasEvent::Scrolled(..)
|
||||
| behavior::topology::CanvasEvent::Focus(..)
|
||||
)
|
||||
| Message::ScenarioCanvas(
|
||||
behavior::scenario::CanvasEvent::Camera(..)
|
||||
| behavior::scenario::CanvasEvent::Zoom(..)
|
||||
| behavior::scenario::CanvasEvent::Scrolled(..)
|
||||
| behavior::scenario::CanvasEvent::Focus(..)
|
||||
)
|
||||
| Message::StateDiagram(
|
||||
behavior::state_edit::Event::Pan(..)
|
||||
| behavior::state_edit::Event::Zoom(..)
|
||||
| behavior::state_edit::Event::Fit(..)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn finish_diagram_reveal(&mut self, surface: Surface) {
|
||||
let Some((context, usage, _)) = self.pending_diagram_reveal.take() else {
|
||||
return;
|
||||
};
|
||||
if context.document == self.canvas_document
|
||||
&& context.revision == self.document.revision()
|
||||
&& usage.surface == surface
|
||||
&& !self.draft_dirty()
|
||||
&& !self.source_dirty()
|
||||
&& !self.behavior.has_pending()
|
||||
{
|
||||
// The normal viewport handler has now stored the measured size.
|
||||
// Target identity was validated for this revision before navigation.
|
||||
self.reveal_diagram_usage(&usage);
|
||||
}
|
||||
self.pending_diagram_reveal = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
//! Independent first-layout navigation oracles, through the real App boundary.
|
||||
use super::*;
|
||||
use crate::diagram_usage::{self as usage, Target, Usage};
|
||||
use iced::{Size, Vector};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Canvas {
|
||||
Architecture,
|
||||
Process,
|
||||
Scenario,
|
||||
State,
|
||||
}
|
||||
const CANVASES: [Canvas; 4] = [
|
||||
Canvas::Architecture,
|
||||
Canvas::Process,
|
||||
Canvas::Scenario,
|
||||
Canvas::State,
|
||||
];
|
||||
const WIDE: Size = Size::new(1200., 700.);
|
||||
const ACTUAL: Size = Size::new(500., 300.);
|
||||
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
fn fixture(canvas: Canvas) -> (App, Usage) {
|
||||
let mut p = Project::empty();
|
||||
let work = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
|
||||
.unwrap();
|
||||
let (surface, subject) = match canvas {
|
||||
Canvas::Architecture => {
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
p.place(
|
||||
diagram,
|
||||
work,
|
||||
Position {
|
||||
x: 12_000.,
|
||||
y: 9_000.,
|
||||
},
|
||||
);
|
||||
(Surface::Saved(diagram), work)
|
||||
}
|
||||
Canvas::Process | Canvas::Scenario => {
|
||||
let kind = if matches!(canvas, Canvas::Process) {
|
||||
Kind::Process
|
||||
} else {
|
||||
Kind::Scenario
|
||||
};
|
||||
let owner = p.add_element(kind, Layer::Oa, "Response", None).unwrap();
|
||||
let occurrence = p
|
||||
.add_element(Kind::Occurrence, Layer::Oa, "Respond here", Some(owner))
|
||||
.unwrap();
|
||||
*p.elements
|
||||
.get_mut(&occurrence)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: None,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
let steps = p.elements.get_mut(&owner).unwrap().steps_mut().unwrap();
|
||||
if !steps.contains(&occurrence) {
|
||||
steps.push(occurrence);
|
||||
}
|
||||
(
|
||||
if matches!(canvas, Canvas::Process) {
|
||||
Surface::Process(owner)
|
||||
} else {
|
||||
Surface::Scenario(owner)
|
||||
},
|
||||
occurrence,
|
||||
)
|
||||
}
|
||||
Canvas::State => {
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Modes", None)
|
||||
.unwrap();
|
||||
let region = p
|
||||
.add_element(Kind::Region, Layer::Oa, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let state = p
|
||||
.add_element(Kind::State, Layer::Oa, "Ready", Some(region))
|
||||
.unwrap();
|
||||
let diagram = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias: "ready_view".into(),
|
||||
name: "Ready view".into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::StateMachine { machine },
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
view_options: Default::default(),
|
||||
},
|
||||
);
|
||||
p.place(
|
||||
diagram,
|
||||
state,
|
||||
Position {
|
||||
x: 8_000.,
|
||||
y: 11_000.,
|
||||
},
|
||||
);
|
||||
(Surface::Saved(diagram), state)
|
||||
}
|
||||
};
|
||||
let mut app = App::with_project(p);
|
||||
let found = usage::collect(
|
||||
app.project(),
|
||||
subject,
|
||||
&app.behavior.topology,
|
||||
&app.behavior.scenario,
|
||||
)
|
||||
.into_iter()
|
||||
.find(|u| u.surface == surface && u.target != Target::Whole)
|
||||
.unwrap();
|
||||
app.canvas_size = Some(WIDE);
|
||||
app.behavior.diagram.viewport = Some(WIDE);
|
||||
if let Surface::Process(owner) = surface {
|
||||
app.behavior.topology.enter(app.canvas_document, owner);
|
||||
app.behavior.topology.viewport(WIDE);
|
||||
}
|
||||
if let Surface::Scenario(owner) = surface {
|
||||
app.behavior.scenario.enter(app.canvas_document, owner);
|
||||
app.behavior.scenario.viewport(WIDE);
|
||||
}
|
||||
app.page = Page::Text;
|
||||
(app, found)
|
||||
}
|
||||
|
||||
fn measurement(
|
||||
canvas: Canvas,
|
||||
context: Context,
|
||||
epoch: u64,
|
||||
surface: Surface,
|
||||
size: Size,
|
||||
) -> Message {
|
||||
match canvas {
|
||||
Canvas::Architecture => Message::DiagramRevealViewport(context, epoch, size),
|
||||
Canvas::Process => Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(
|
||||
behavior::topology::Context {
|
||||
document: context.document,
|
||||
owner: surface.id(),
|
||||
revision: context.revision,
|
||||
epoch,
|
||||
},
|
||||
size,
|
||||
)),
|
||||
Canvas::Scenario => Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(
|
||||
behavior::scenario::Context {
|
||||
document: context.document,
|
||||
scenario: surface.id(),
|
||||
revision: context.revision,
|
||||
epoch,
|
||||
},
|
||||
size,
|
||||
)),
|
||||
Canvas::State => Message::StateDiagram(behavior::state_edit::Event::Viewport(
|
||||
behavior::state_edit::Context {
|
||||
document: context.document,
|
||||
diagram: surface.id(),
|
||||
revision: context.revision,
|
||||
epoch,
|
||||
},
|
||||
size,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn stamp(app: &App) -> (Context, u64) {
|
||||
let (context, _, epoch) = app
|
||||
.pending_diagram_reveal
|
||||
.as_ref()
|
||||
.expect("reveal awaits measured destination");
|
||||
(*context, *epoch)
|
||||
}
|
||||
|
||||
fn assert_centered(app: &App, canvas: Canvas, target: &Usage) {
|
||||
// All independently authored fixture targets fit ACTUAL at the readable cap.
|
||||
// This oracle does not call the production camera/reveal implementation.
|
||||
let zoom = 1.25;
|
||||
let pan = Vector::new(
|
||||
250. - target.bounds.center_x() * zoom,
|
||||
150. - target.bounds.center_y() * zoom,
|
||||
);
|
||||
match canvas {
|
||||
Canvas::Architecture => {
|
||||
assert_eq!(app.canvas_size, Some(ACTUAL));
|
||||
assert_eq!(app.zoom, zoom);
|
||||
assert_eq!(app.pan, pan);
|
||||
assert!(!app.fit_to_view);
|
||||
}
|
||||
Canvas::State => {
|
||||
assert_eq!(app.behavior.diagram.viewport, Some(ACTUAL));
|
||||
assert_eq!(app.behavior.diagram.zoom, zoom);
|
||||
assert_eq!(app.behavior.diagram.pan, pan);
|
||||
assert!(!app.behavior.diagram.fit);
|
||||
}
|
||||
Canvas::Process => {
|
||||
let mut expected = behavior::topology::State::default();
|
||||
expected.enter(app.canvas_document, target.surface.id());
|
||||
expected.viewport(ACTUAL);
|
||||
expected.zoom(Some(zoom));
|
||||
expected.camera(behavior::canvas_workspace::Event::Pan(pan));
|
||||
assert!(app.behavior.topology.camera_boundary() == expected.camera_boundary());
|
||||
}
|
||||
Canvas::Scenario => {
|
||||
let mut expected = behavior::scenario::State::default();
|
||||
expected.enter(app.canvas_document, target.surface.id());
|
||||
expected.choose_tool(target.surface.id(), behavior::scenario::CanvasTool::Select);
|
||||
expected.viewport(ACTUAL);
|
||||
expected.zoom(Some(zoom));
|
||||
expected.camera(behavior::canvas_workspace::Event::Pan(pan));
|
||||
assert!(app.behavior.scenario.camera_boundary() == expected.camera_boundary());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_reveal_surfaces_recenter_once_after_the_real_viewport_and_preserve_authored_state() {
|
||||
for canvas in CANVASES {
|
||||
let (mut app, target) = fixture(canvas);
|
||||
let accepted = app.document.snapshot();
|
||||
let drafts = app.source_workspace.bundle();
|
||||
let revision = app.document.revision();
|
||||
let history = (
|
||||
app.document.can_undo(),
|
||||
app.document.can_redo(),
|
||||
app.document.dirty(),
|
||||
);
|
||||
assert!(app.reveal_diagram_usage(&target));
|
||||
let (context, epoch) = stamp(&app);
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, context, epoch, target.surface, ACTUAL),
|
||||
);
|
||||
assert!(app.pending_diagram_reveal.is_none());
|
||||
assert_centered(&app, canvas, &target);
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert_eq!(app.source_workspace.bundle(), drafts);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert_eq!(
|
||||
(
|
||||
app.document.can_undo(),
|
||||
app.document.can_redo(),
|
||||
app.document.dirty()
|
||||
),
|
||||
history
|
||||
);
|
||||
// The exact delivered callback is a replay after the one-shot is consumed.
|
||||
let camera = app.camera_boundary();
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, context, epoch, target.surface, ACTUAL),
|
||||
);
|
||||
assert!(app.pending_diagram_reveal.is_none());
|
||||
assert!(app.camera_boundary() == camera);
|
||||
assert_centered(&app, canvas, &target);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_wrong_stamp_measurements_cannot_poison_a_pending_reveal() {
|
||||
for canvas in CANVASES {
|
||||
for invalid in 0..6 {
|
||||
let (mut app, target) = fixture(canvas);
|
||||
assert!(app.reveal_diagram_usage(&target));
|
||||
let (context, epoch) = stamp(&app);
|
||||
let camera = app.camera_boundary();
|
||||
let mut stale = context;
|
||||
let mut wrong_epoch = epoch;
|
||||
let mut size = Size::new(111., 122.);
|
||||
match invalid {
|
||||
0 => stale.document = Id::new_v4(),
|
||||
1 => stale.revision = stale.revision.wrapping_add(1),
|
||||
2 => wrong_epoch = wrong_epoch.wrapping_add(1),
|
||||
3 => size.width = f32::NAN,
|
||||
4 => size.height = f32::INFINITY,
|
||||
_ => size.width = 0.,
|
||||
}
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, stale, wrong_epoch, target.surface, size),
|
||||
);
|
||||
assert!(
|
||||
app.pending_diagram_reveal.is_some(),
|
||||
"{canvas:?} invalid case {invalid}"
|
||||
);
|
||||
assert_eq!(stamp(&app), (context, epoch));
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
assert!(
|
||||
app.camera_boundary() == camera,
|
||||
"{canvas:?} invalid case {invalid}"
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, context, epoch, target.surface, ACTUAL),
|
||||
);
|
||||
assert!(app.pending_diagram_reveal.is_none());
|
||||
assert_centered(&app, canvas, &target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_window_resize_before_first_measurement_retains_focus_for_the_new_context() {
|
||||
for canvas in CANVASES {
|
||||
let (mut app, target) = fixture(canvas);
|
||||
let accepted = app.document.snapshot();
|
||||
assert!(app.reveal_diagram_usage(&target));
|
||||
let (context, old_epoch) = stamp(&app);
|
||||
send(&mut app, Message::WindowResized(Size::new(1100., 700.)));
|
||||
let (current, epoch) = stamp(&app);
|
||||
assert_eq!(current, context);
|
||||
assert_eq!(epoch, app.canvas_epoch);
|
||||
if matches!(canvas, Canvas::Process | Canvas::Scenario) {
|
||||
assert_ne!(epoch, old_epoch);
|
||||
let camera = app.camera_boundary();
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, context, old_epoch, target.surface, ACTUAL),
|
||||
);
|
||||
assert_eq!(stamp(&app), (current, epoch));
|
||||
assert!(app.camera_boundary() == camera);
|
||||
}
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, current, epoch, target.surface, ACTUAL),
|
||||
);
|
||||
assert!(app.pending_diagram_reveal.is_none());
|
||||
assert_centered(&app, canvas, &target);
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_camera_edit_and_navigation_cancel_deferred_focus_without_late_snapping() {
|
||||
for canvas in CANVASES {
|
||||
for action in 0..3 {
|
||||
let (mut app, target) = fixture(canvas);
|
||||
let accepted = app.document.snapshot();
|
||||
assert!(app.reveal_diagram_usage(&target));
|
||||
let (context, epoch) = stamp(&app);
|
||||
let event = match action {
|
||||
0 => match canvas {
|
||||
Canvas::Architecture => Message::Pan(Vector::new(31., -17.)),
|
||||
Canvas::State => Message::StateDiagram(behavior::state_edit::Event::Pan(
|
||||
app.state_context().unwrap(),
|
||||
Vector::new(31., -17.),
|
||||
)),
|
||||
Canvas::Process => {
|
||||
Message::TopologyCanvas(behavior::topology::CanvasEvent::Camera(
|
||||
app.topology_context().unwrap(),
|
||||
app.camera_epoch,
|
||||
behavior::canvas_workspace::Event::Pan(Vector::new(31., -17.)),
|
||||
))
|
||||
}
|
||||
Canvas::Scenario => {
|
||||
Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Camera(
|
||||
app.scenario_context().unwrap(),
|
||||
app.camera_epoch,
|
||||
behavior::canvas_workspace::Event::Pan(Vector::new(31., -17.)),
|
||||
))
|
||||
}
|
||||
},
|
||||
1 => Message::Field("/name".into(), "Pending title edit".into()),
|
||||
_ => Message::Page(Page::Catalog),
|
||||
};
|
||||
send(&mut app, event);
|
||||
assert!(
|
||||
app.pending_diagram_reveal.is_none(),
|
||||
"{canvas:?} action {action}"
|
||||
);
|
||||
let camera = app.camera_boundary();
|
||||
let page = app.page;
|
||||
// A stale layout callback at the already retained size cannot
|
||||
// restore the old navigation camera after deliberate user input.
|
||||
send(
|
||||
&mut app,
|
||||
measurement(canvas, context, epoch, target.surface, WIDE),
|
||||
);
|
||||
assert!(app.pending_diagram_reveal.is_none());
|
||||
assert!(
|
||||
app.camera_boundary() == camera,
|
||||
"{canvas:?} action {action}"
|
||||
);
|
||||
assert_eq!(app.page, page);
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
if action == 1 {
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
//! Navigation-only adapter: validates destinations before changing presentation.
|
||||
use super::*;
|
||||
use crate::diagram_usage::{self as usage, Context, Event, Panel, Surface, Target, Usage};
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
impl App {
|
||||
pub(crate) fn diagram_usage_message(&self, subject: Id) -> Message {
|
||||
Message::DiagramUsage(Event::Open(Context {
|
||||
document: self.canvas_document,
|
||||
revision: self.document.revision(),
|
||||
subject,
|
||||
}))
|
||||
}
|
||||
|
||||
fn usage_navigation_available(&self) -> bool {
|
||||
!self.busy
|
||||
&& self.pending.is_none()
|
||||
&& self.file_prompt.is_none()
|
||||
&& !self.delete_pending
|
||||
&& self.workflow.is_none()
|
||||
&& !self.hosted.visible
|
||||
&& self.architecture.is_none()
|
||||
&& !self.reference_input_active()
|
||||
&& !self.behavior.has_pending()
|
||||
&& !self.draft_dirty()
|
||||
&& !self.source_dirty()
|
||||
}
|
||||
|
||||
/// Admit at most one expensive lookup, including one accepted Project clone.
|
||||
/// A replacement panel waits for the previous worker's exact completion.
|
||||
fn start_diagram_lookup(&mut self) -> Task<Message> {
|
||||
let Some(panel) = self.diagram_usages.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
if !panel.loading || self.diagram_lookup_in_flight.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
let (token, context) = (panel.token, panel.context);
|
||||
if context.document != self.canvas_document
|
||||
|| context.revision != self.document.revision()
|
||||
|| !self.usage_navigation_available()
|
||||
{
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
panel.loading = false;
|
||||
panel.notice = Some(
|
||||
"The model or pending edits changed. Reopen diagram navigation when ready.".into(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let project = self.project().clone();
|
||||
let presentation = self.behavior.topology.usage_snapshot();
|
||||
self.diagram_lookup_in_flight = Some(token);
|
||||
Task::perform(
|
||||
async move {
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let process = presentation.into_query_state();
|
||||
let scenario = behavior::scenario::State::default();
|
||||
usage::collect(&project, context.subject, &process, &scenario)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("Diagram lookup failed: {error}. Close and try again."));
|
||||
usage::Completion {
|
||||
token,
|
||||
context,
|
||||
result,
|
||||
}
|
||||
},
|
||||
Message::DiagramUsagesReady,
|
||||
)
|
||||
}
|
||||
|
||||
fn refresh_diagram_lookup(&mut self, notice: &str) -> Task<Message> {
|
||||
let Some(panel) = self.diagram_usages.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
panel.token = Id::new_v4();
|
||||
panel.context.revision = self.document.revision();
|
||||
panel.usages.clear();
|
||||
panel.loading = true;
|
||||
panel.page = 0;
|
||||
panel.cursor = 0;
|
||||
panel.destination = None;
|
||||
panel.notice = Some(notice.into());
|
||||
self.start_diagram_lookup()
|
||||
}
|
||||
|
||||
/// Must run before modal/input dispatch. A closed or replaced panel still
|
||||
/// owns its worker slot until this exact completion arrives.
|
||||
pub(super) fn diagram_usages_ready(&mut self, completion: usage::Completion) -> Task<Message> {
|
||||
if self.diagram_lookup_in_flight != Some(completion.token) {
|
||||
return Task::none();
|
||||
}
|
||||
self.diagram_lookup_in_flight = None;
|
||||
let Some(panel) = self.diagram_usages.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
if panel.context.document != self.canvas_document {
|
||||
self.diagram_usages = None;
|
||||
return Task::none();
|
||||
}
|
||||
if panel.context.revision != self.document.revision() {
|
||||
return self
|
||||
.refresh_diagram_lookup("The model changed. Review the refreshed destinations.");
|
||||
}
|
||||
if panel.token != completion.token {
|
||||
// A newer Open/refresh waited without cloning or spawning a second job.
|
||||
return self.start_diagram_lookup();
|
||||
}
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
if panel.context != completion.context || !panel.loading {
|
||||
panel.loading = false;
|
||||
panel.notice = Some(
|
||||
"This lookup no longer matches the requested model. Close and try again.".into(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
panel.loading = false;
|
||||
match completion.result {
|
||||
Ok(usages) => panel.usages = usages,
|
||||
Err(error) => panel.notice = Some(error),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Selection validates one destination only; opening the chooser does the
|
||||
/// whole-project scan on its worker, never on the GUI thread.
|
||||
fn current_surface_usages(&self, subject: Id, surface: Surface) -> Vec<Usage> {
|
||||
match surface {
|
||||
Surface::Saved(diagram) => {
|
||||
usage::saved_usages_for(self.project(), subject, Some(diagram))
|
||||
}
|
||||
Surface::Process(owner) => {
|
||||
self.behavior
|
||||
.topology
|
||||
.diagram_usages(self.project(), owner, subject)
|
||||
}
|
||||
Surface::Scenario(owner) => {
|
||||
self.behavior
|
||||
.scenario
|
||||
.diagram_usages(self.project(), owner, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn diagram_usage_event(&mut self, event: Event) -> Task<Message> {
|
||||
if let Event::Open(context) = event {
|
||||
if context.document != self.canvas_document
|
||||
|| context.revision != self.document.revision()
|
||||
|| !self.project().contains(context.subject)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if !self.usage_navigation_available() {
|
||||
self.status = "Apply or revert pending edits before opening diagram navigation. Your inputs are retained.".into();
|
||||
return Task::none();
|
||||
}
|
||||
let mut panel = Panel::new(context, Vec::new());
|
||||
panel.loading = true;
|
||||
let filter_id = format!("diagram-usage-filter-{}", panel.token);
|
||||
self.diagram_usages = Some(panel);
|
||||
self.model_menu = None;
|
||||
// Only an accepted new chooser supersedes the previous reveal.
|
||||
// Rejected/stale panel envelopes leave its first measurement intact.
|
||||
self.pending_diagram_reveal = None;
|
||||
self.diagram_navigation_focus = None;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
return Task::batch([
|
||||
self.start_diagram_lookup(),
|
||||
iced::widget::operation::focus(filter_id),
|
||||
]);
|
||||
}
|
||||
let token = match &event {
|
||||
Event::Open(_) => unreachable!(),
|
||||
Event::Close(t)
|
||||
| Event::Search(t, _)
|
||||
| Event::Page(t, _)
|
||||
| Event::Move(t, _)
|
||||
| Event::Activate(t)
|
||||
| Event::Destination(t, _)
|
||||
| Event::Choose(t, _) => *t,
|
||||
};
|
||||
let Some(panel) = &self.diagram_usages else {
|
||||
return Task::none();
|
||||
};
|
||||
if panel.token != token {
|
||||
return Task::none();
|
||||
}
|
||||
if matches!(event, Event::Close(_)) {
|
||||
self.diagram_usages = None;
|
||||
if self.page == Page::Text {
|
||||
self.restore_source_editor_focus();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if panel.context.document != self.canvas_document {
|
||||
self.diagram_usages = None;
|
||||
return Task::none();
|
||||
}
|
||||
if panel.context.revision != self.document.revision() {
|
||||
return self
|
||||
.refresh_diagram_lookup("The model changed. Review the refreshed destinations.");
|
||||
}
|
||||
if panel.loading {
|
||||
// Search and Close remain useful while loading; queued activation
|
||||
// must not perform a synchronous fallback or choose an old row.
|
||||
if let Event::Search(_, query) = event {
|
||||
self.diagram_usages.as_mut().unwrap().search = query;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
match event {
|
||||
Event::Move(_, direction) => {
|
||||
let panel = self.diagram_usages.as_ref().unwrap();
|
||||
let count = panel.choices(self.project()).len();
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
if count > 0 {
|
||||
panel.cursor = if direction < 0 {
|
||||
(panel.cursor + count - 1) % count
|
||||
} else {
|
||||
(panel.cursor + 1) % count
|
||||
};
|
||||
panel.page = panel.cursor / 6;
|
||||
}
|
||||
}
|
||||
Event::Activate(_) => {
|
||||
let panel = self.diagram_usages.as_ref().unwrap();
|
||||
if let Some(event) = panel.choices(self.project()).get(panel.cursor).cloned() {
|
||||
return self.diagram_usage_event(event);
|
||||
}
|
||||
}
|
||||
Event::Search(_, query) => {
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
panel.search = query;
|
||||
panel.page = 0;
|
||||
panel.cursor = 0;
|
||||
}
|
||||
Event::Page(_, page) => {
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
panel.page = page;
|
||||
panel.cursor = page.saturating_mul(6);
|
||||
}
|
||||
Event::Destination(_, surface) => {
|
||||
let panel = self.diagram_usages.as_mut().unwrap();
|
||||
if surface.is_none() || panel.usages.iter().any(|u| Some(u.surface) == surface) {
|
||||
panel.destination = surface;
|
||||
panel.page = 0;
|
||||
panel.cursor = 0;
|
||||
}
|
||||
}
|
||||
Event::Choose(_, requested) => {
|
||||
if !self.usage_navigation_available() {
|
||||
return Task::none();
|
||||
}
|
||||
let subject = self.diagram_usages.as_ref().unwrap().context.subject;
|
||||
let fresh = self
|
||||
.current_surface_usages(subject, requested.surface)
|
||||
.into_iter()
|
||||
.find(|u| u.subject == requested.subject && u.target == requested.target);
|
||||
let Some(fresh) = fresh else {
|
||||
return self.refresh_diagram_lookup(
|
||||
"This representation changed. Review the refreshed destinations.",
|
||||
);
|
||||
};
|
||||
if self.reveal_diagram_usage(&fresh) {
|
||||
self.diagram_usages = None;
|
||||
self.status = format!(
|
||||
"{} · {} in {}",
|
||||
self.project().named(subject),
|
||||
fresh.role,
|
||||
self.project().named(fresh.surface.id())
|
||||
);
|
||||
return Task::batch(["model-explorer", "model-browser", "inspector"].map(
|
||||
|id| {
|
||||
iced::widget::operation::snap_to(
|
||||
id,
|
||||
iced::widget::scrollable::RelativeOffset::START,
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
Event::Open(_) | Event::Close(_) => unreachable!(),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn reveal_diagram_usage(&mut self, usage: &Usage) -> bool {
|
||||
if !self.usage_navigation_available() {
|
||||
return false;
|
||||
}
|
||||
let previous_layer = self.layer;
|
||||
let p = self.document.project();
|
||||
let (selected, placement) = match &usage.target {
|
||||
Target::Whole => (usage.surface.id(), None),
|
||||
Target::Placement(id) => {
|
||||
let Some(node) = p
|
||||
.diagrams
|
||||
.get(&usage.surface.id())
|
||||
.and_then(|d| d.nodes.get(id))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
(node.element, Some(*id))
|
||||
}
|
||||
Target::Port { port, placement } => (*port, Some(*placement)),
|
||||
Target::StateRow {
|
||||
placement,
|
||||
relation,
|
||||
} => (*relation, Some(*placement)),
|
||||
Target::Edge(id)
|
||||
| Target::ProcessLink(id)
|
||||
| Target::ScenarioObject(id)
|
||||
| Target::AllocationGrouping { relation: id, .. }
|
||||
| Target::ProcessPreviewLink { relation: id, .. } => (*id, None),
|
||||
Target::ProcessOccurrence(endpoint) => (
|
||||
endpoint
|
||||
.references
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(endpoint.occurrence),
|
||||
None,
|
||||
),
|
||||
};
|
||||
match usage.surface {
|
||||
Surface::Saved(id) => {
|
||||
let Some(diagram) = p.diagrams.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
self.layer = diagram.layer;
|
||||
self.active_diagram = Some(id);
|
||||
if let Some(machine) = diagram.projection.state_machine() {
|
||||
let viewport = self.behavior.diagram.viewport.unwrap_or(iced::Size::new(
|
||||
(self.window_size.width - 610.).max(280.),
|
||||
(self.window_size.height - 320.).max(240.),
|
||||
));
|
||||
let (zoom, pan) = usage::camera_for(&usage.target, usage.bounds, viewport);
|
||||
self.behavior.diagram = behavior::state_view::State {
|
||||
diagram: Some(id),
|
||||
placement,
|
||||
viewport: Some(viewport),
|
||||
zoom,
|
||||
pan,
|
||||
fit: false,
|
||||
..Default::default()
|
||||
};
|
||||
self.behavior.active_owner = Some(machine);
|
||||
self.page = Page::Behavior;
|
||||
} else {
|
||||
let viewport = self.canvas_size.unwrap_or(iced::Size::new(
|
||||
(self.window_size.width - 550.).max(280.),
|
||||
(self.window_size.height - 284.).max(240.),
|
||||
));
|
||||
(self.zoom, self.pan) =
|
||||
usage::camera_for(&usage.target, usage.bounds, viewport);
|
||||
self.fit_to_view = false;
|
||||
self.page = Page::Diagram;
|
||||
}
|
||||
}
|
||||
Surface::Process(owner) => {
|
||||
if !self.behavior.topology.reveal_usage(
|
||||
p,
|
||||
self.canvas_document,
|
||||
owner,
|
||||
&usage.target,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
self.layer = p.elements[&owner].layer();
|
||||
self.behavior.active_owner = Some(owner);
|
||||
self.page = Page::Behavior;
|
||||
}
|
||||
Surface::Scenario(owner) => {
|
||||
if !self.behavior.scenario.reveal_usage(
|
||||
p,
|
||||
self.canvas_document,
|
||||
owner,
|
||||
&usage.target,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
self.layer = p.elements[&owner].layer();
|
||||
self.behavior.active_owner = Some(owner);
|
||||
self.page = Page::Behavior;
|
||||
}
|
||||
}
|
||||
self.selected = selected;
|
||||
self.selected_node = placement;
|
||||
self.model_menu = None;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.selected_bend = None;
|
||||
self.connection = None;
|
||||
self.last_workspace_page = self.page;
|
||||
if previous_layer != self.layer {
|
||||
self.data_state = Default::default();
|
||||
self.explorer_index = crate::explorer::Index::new(self.project(), self.layer);
|
||||
self.explorer_focus = None;
|
||||
self.explorer_group = None;
|
||||
self.explorer_page = 0;
|
||||
}
|
||||
self.normalize_creation();
|
||||
self.reveal_explorer_selection();
|
||||
self.refresh_draft();
|
||||
self.diagram_navigation_focus = Some(usage::Focus {
|
||||
context: Context {
|
||||
document: self.canvas_document,
|
||||
revision: self.document.revision(),
|
||||
subject: usage.subject,
|
||||
},
|
||||
usage: usage.clone(),
|
||||
selected,
|
||||
});
|
||||
self.pending_diagram_reveal = Some((
|
||||
Context {
|
||||
document: self.canvas_document,
|
||||
revision: self.document.revision(),
|
||||
subject: usage.subject,
|
||||
},
|
||||
usage.clone(),
|
||||
self.canvas_epoch,
|
||||
));
|
||||
true
|
||||
}
|
||||
|
||||
/// Validated presentation identity. Edit selection and Delete capture remain
|
||||
/// semantic; a port owner or allocation work box is only a visual marker.
|
||||
pub(crate) fn diagram_focus(&self, surface: Surface) -> Option<&Usage> {
|
||||
let focus = self.diagram_navigation_focus.as_ref()?;
|
||||
let current_surface = match surface {
|
||||
Surface::Saved(id) => {
|
||||
self.active_diagram == Some(id)
|
||||
&& matches!(self.page, Page::Diagram | Page::Behavior)
|
||||
}
|
||||
Surface::Process(owner) | Surface::Scenario(owner) => {
|
||||
self.page == Page::Behavior && self.behavior.active_owner == Some(owner)
|
||||
}
|
||||
};
|
||||
(current_surface
|
||||
&& focus.usage.surface == surface
|
||||
&& focus.context.document == self.canvas_document
|
||||
&& focus.context.revision == self.document.revision()
|
||||
&& focus.selected == self.selected)
|
||||
.then_some(&focus.usage)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -213,6 +213,14 @@ impl App {
|
||||
self.pending = None;
|
||||
return Task::none();
|
||||
}
|
||||
if self.path.is_some()
|
||||
|| self.hosted.session.as_ref().is_none_or(|session| {
|
||||
session.resource_id != remote.resource_id
|
||||
|| session.connection.base() != request.connection.base()
|
||||
})
|
||||
{
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.hosted.session = Some(Session {
|
||||
connection: request.connection.clone(),
|
||||
resource_id: remote.resource_id,
|
||||
@@ -345,6 +353,14 @@ impl App {
|
||||
match self.document.apply_compiled((*remote.compiled).clone()) {
|
||||
Ok(_) => {
|
||||
self.document.mark_saved(remote.compiled.clone());
|
||||
if self.path.is_some()
|
||||
|| self.hosted.session.as_ref().is_none_or(|session| {
|
||||
session.resource_id != remote.resource_id
|
||||
|| session.connection.base() != connection.base()
|
||||
})
|
||||
{
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.hosted.session = Some(Session {
|
||||
connection,
|
||||
resource_id: remote.resource_id,
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
//! Current draft navigation, isolated from model application and disk discovery.
|
||||
use super::*;
|
||||
use crate::source_navigation::{self as nav, query};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
fn admission_notice(error: query::AdmissionError) -> String {
|
||||
use nav::symbols::{BuildError, InputProblem, Resource};
|
||||
let (limit, resource) = match error {
|
||||
query::AdmissionError::Index(BuildError::Limit { limit, resource, .. }) => (limit, resource),
|
||||
query::AdmissionError::Outline { error, .. } => (error.limit, match error.resource {
|
||||
language::OutlineResource::SourceBytes => Resource::SourceBytes,
|
||||
language::OutlineResource::Tokens => Resource::Tokens,
|
||||
language::OutlineResource::Declarations => Resource::Records,
|
||||
}),
|
||||
query::AdmissionError::Index(BuildError::Input { problem, .. }) => return match problem {
|
||||
InputProblem::NonCanonicalPath | InputProblem::DuplicateOrCaseCollidingPath =>
|
||||
"Navigation needs distinct relative source paths. Check file names and include paths.".into(),
|
||||
_ => "Navigation could not verify the current source locations. Correct source errors and try again.".into(),
|
||||
},
|
||||
};
|
||||
let unit = match resource {
|
||||
Resource::Files => "loaded files",
|
||||
Resource::SourceBytes => "source bytes",
|
||||
Resource::PathBytes => "bytes in file paths",
|
||||
Resource::Tokens => "source tokens",
|
||||
Resource::Records => "declarations and references",
|
||||
};
|
||||
format!(
|
||||
"Navigation reached its limit of {limit} {unit}. Reduce the loaded source size and try again."
|
||||
)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn source_symbol_context(&self) -> nav::SymbolContext {
|
||||
nav::SymbolContext {
|
||||
session: self.source_workspace.session(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source_symbol_message(&self, event: nav::SymbolEvent) -> Message {
|
||||
Message::SourceSymbols(self.source_symbol_context(), event)
|
||||
}
|
||||
|
||||
pub fn source_find_close_message(&self) -> Message {
|
||||
Message::SourceFindClose(self.source_symbol_context(), self.source_find_instance)
|
||||
}
|
||||
pub fn source_key(&self) -> query::SnapshotKey {
|
||||
query::SnapshotKey {
|
||||
workspace: self.source_workspace.session(),
|
||||
revision: self.source_workspace.revision(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_source_input<T>(&self, use_input: impl FnOnce(query::Input<'_>) -> T) -> T {
|
||||
let files: Vec<_> = self
|
||||
.source_workspace
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, buffer)| query::DraftFile {
|
||||
path,
|
||||
text: &buffer.text,
|
||||
})
|
||||
.collect();
|
||||
let accepted = self.document.bundle();
|
||||
let validated: Vec<_> = accepted
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, text)| query::DraftFile { path, text })
|
||||
.collect();
|
||||
use_input(query::Input {
|
||||
key: self.source_key(),
|
||||
entry: &self.source_workspace.entry,
|
||||
files: &files,
|
||||
applied: self.source_workspace.applied,
|
||||
validation: self
|
||||
.source_workspace
|
||||
.applied
|
||||
.then_some(query::ValidatedSources {
|
||||
entry: &accepted.entry,
|
||||
files: &validated,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn source_snapshot_matches(&self, snapshot: &query::Snapshot) -> bool {
|
||||
snapshot.key() == self.source_key()
|
||||
&& self.with_source_input(|input| snapshot.matches(input))
|
||||
}
|
||||
|
||||
pub fn source_navigation_available(&self) -> bool {
|
||||
self.page == Page::Text
|
||||
&& !self.busy
|
||||
&& self.pending.is_none()
|
||||
&& self.file_prompt.is_none()
|
||||
&& !self.delete_pending
|
||||
&& self.workflow.is_none()
|
||||
&& !self.hosted.visible
|
||||
&& self.model_menu.is_none()
|
||||
&& self.diagram_usages.is_none()
|
||||
&& !self.reference_input_active()
|
||||
&& self.architecture.is_none()
|
||||
&& !self.behavior.has_pending()
|
||||
}
|
||||
|
||||
/// Mounted widget IDs change when another interaction supersedes a reveal.
|
||||
/// A queued focus operation therefore cannot find a subsequently reused editor.
|
||||
pub fn source_editor_id(&self) -> iced::widget::Id {
|
||||
iced::widget::Id::from(format!(
|
||||
"source-editor-{}-{}-{}",
|
||||
self.source_workspace.session(),
|
||||
self.source_workspace.active,
|
||||
self.source_symbols.interaction
|
||||
))
|
||||
}
|
||||
|
||||
pub fn source_reveal_ticket(&self) -> Option<Id> {
|
||||
self.source_symbols
|
||||
.reveal
|
||||
.as_ref()
|
||||
.map(|reveal| reveal.ticket)
|
||||
}
|
||||
|
||||
pub(super) fn source_interaction(&mut self) {
|
||||
self.source_symbols.interaction = Id::new_v4();
|
||||
self.source_symbols.request = None;
|
||||
self.source_symbols.reveal = None;
|
||||
self.source_jump = None;
|
||||
}
|
||||
|
||||
pub(super) fn queue_source_reveal(&mut self, cursor: text_editor::Cursor, select_line: bool) {
|
||||
self.source_jump = Some(cursor);
|
||||
self.source_select_line = select_line;
|
||||
self.source_symbols.reveal = Some(nav::Reveal {
|
||||
ticket: Id::new_v4(),
|
||||
interaction: self.source_symbols.interaction,
|
||||
raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
snapshot: self.source_key(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn restore_source_editor_focus(&mut self) {
|
||||
let cursor = self.source_editor.cursor();
|
||||
if self.source_navigation_available()
|
||||
&& nav::exact_cursor(self.source_workspace.text(), cursor)
|
||||
{
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn source_viewport(&mut self, ticket: Option<Id>) -> Task<Message> {
|
||||
let Some(reveal) = &self.source_symbols.reveal else {
|
||||
return Task::none();
|
||||
};
|
||||
if Some(reveal.ticket) != ticket
|
||||
|| reveal.interaction != self.source_symbols.interaction
|
||||
|| !nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
reveal.raw_interaction,
|
||||
)
|
||||
|| reveal.snapshot != self.source_key()
|
||||
|| reveal.path != self.source_workspace.active
|
||||
|| !self.source_navigation_available()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let raw_interaction = reveal.raw_interaction;
|
||||
self.source_symbols.reveal = None;
|
||||
if let Some(cursor) = self.source_jump.take() {
|
||||
if !nav::exact_cursor(self.source_workspace.text(), cursor) {
|
||||
return Task::none();
|
||||
}
|
||||
self.source_editor.move_to(cursor);
|
||||
if self.source_select_line && cursor.selection.is_none() {
|
||||
self.source_editor.perform(text_editor::Action::SelectLine);
|
||||
}
|
||||
return nav::interaction::guarded_focus(
|
||||
self.source_editor_id(),
|
||||
self.source_symbols.raw_interaction.clone(),
|
||||
raw_interaction,
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn source_symbol_event(&mut self, event: nav::SymbolEvent) -> Task<Message> {
|
||||
if !self.source_navigation_available() {
|
||||
return Task::none();
|
||||
}
|
||||
match event {
|
||||
nav::SymbolEvent::DiagramUsages => {
|
||||
if self.source_dirty() || !self.source_workspace.applied {
|
||||
self.source_symbols.open = true;
|
||||
self.source_symbols.notice = Some("Apply source before finding diagram representations. Your current text is retained.".into());
|
||||
return Task::none();
|
||||
}
|
||||
let task = self.start_source_query(query::Action::Definition);
|
||||
self.source_diagram_job = self
|
||||
.source_symbols
|
||||
.request
|
||||
.as_ref()
|
||||
.map(|request| request.job);
|
||||
return task;
|
||||
}
|
||||
nav::SymbolEvent::Close => {
|
||||
self.source_symbols.open = false;
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
}
|
||||
nav::SymbolEvent::Back => {
|
||||
self.source_symbol_back();
|
||||
// A successful Back needs no empty results panel. Retain an
|
||||
// already open panel, or reveal an actionable return notice.
|
||||
self.source_symbols.open |= self.source_symbols.notice.is_some();
|
||||
}
|
||||
nav::SymbolEvent::Choose {
|
||||
stamp,
|
||||
reference,
|
||||
index,
|
||||
} => {
|
||||
let Some(results) = self.source_symbols.results.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
if results.stamp != stamp
|
||||
|| stamp.snapshot != self.source_key()
|
||||
|| self
|
||||
.source_symbols
|
||||
.cache
|
||||
.as_ref()
|
||||
.is_none_or(|cache| !self.source_snapshot_matches(cache))
|
||||
{
|
||||
self.source_symbols.notice =
|
||||
Some("Source changed. Run navigation again for current locations.".into());
|
||||
return Task::none();
|
||||
}
|
||||
let destination = if reference {
|
||||
results
|
||||
.references
|
||||
.get(index)
|
||||
.map(|row| (&row.path, row.position, row.end_position))
|
||||
} else {
|
||||
results
|
||||
.definitions
|
||||
.get(index)
|
||||
.map(|row| (&row.path, row.position, row.end_position))
|
||||
};
|
||||
if let Some((path, start, end)) = destination {
|
||||
self.source_symbol_jump(path, start, end);
|
||||
}
|
||||
}
|
||||
nav::SymbolEvent::Query(action) => return self.start_source_query(action),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn start_source_query(&mut self, action: query::Action) -> Task<Message> {
|
||||
self.source_symbols.open = true;
|
||||
if !nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
) {
|
||||
self.source_symbols.notice = Some("Reopen this workspace to reset navigation.".into());
|
||||
return Task::none();
|
||||
}
|
||||
if self.source_symbols.in_flight.is_some() {
|
||||
self.source_symbols.notice = Some("Navigation is finishing. Try again shortly.".into());
|
||||
return Task::none();
|
||||
}
|
||||
let cursor = self.source_editor.cursor();
|
||||
let Some(offset) = nav::cursor_offset(self.source_workspace.text(), cursor) else {
|
||||
self.source_symbols.notice =
|
||||
Some("Place the cursor on an alias or its @ reference.".into());
|
||||
return Task::none();
|
||||
};
|
||||
self.source_symbols.serial = match self.source_symbols.serial.checked_add(1) {
|
||||
Some(serial) => serial,
|
||||
None => {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
self.source_symbols.back.clear();
|
||||
self.source_symbols.cache = None;
|
||||
0
|
||||
}
|
||||
};
|
||||
let request = nav::Request {
|
||||
job: Id::new_v4(),
|
||||
interaction: self.source_symbols.interaction,
|
||||
raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
stamp: query::QueryStamp {
|
||||
snapshot: self.source_key(),
|
||||
serial: self.source_symbols.serial,
|
||||
},
|
||||
path: self.source_workspace.active.clone(),
|
||||
offset,
|
||||
cursor,
|
||||
action,
|
||||
};
|
||||
let cache = self
|
||||
.source_symbols
|
||||
.cache
|
||||
.as_ref()
|
||||
.filter(|cache| self.source_snapshot_matches(cache))
|
||||
.cloned();
|
||||
let input = if cache.is_none() {
|
||||
match self
|
||||
.with_source_input(|input| query::admit_owned(input, query::Limits::default()))
|
||||
{
|
||||
Ok(input) => Some(input),
|
||||
Err(error) => {
|
||||
self.source_symbols.notice = Some(admission_notice(error));
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.source_symbols.in_flight = Some(request.job);
|
||||
self.source_symbols.request = Some(request.clone());
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
Task::perform(
|
||||
async move {
|
||||
let worker_request = request.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let snapshot = match cache {
|
||||
Some(cache) => cache,
|
||||
None => Arc::new(
|
||||
input
|
||||
.expect("admitted input for uncached request")
|
||||
.capture()
|
||||
.map_err(admission_notice)?,
|
||||
),
|
||||
};
|
||||
let results = snapshot
|
||||
.query(query::Query {
|
||||
stamp: worker_request.stamp,
|
||||
path: &worker_request.path,
|
||||
byte_offset: worker_request.offset,
|
||||
action: worker_request.action,
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
query::QueryError::ResultEnvelopeLimit => "These result details exceed the display limit. Try literal Find for this alias.".to_owned(),
|
||||
_ => "Navigation could not verify this location. Place the cursor on an alias and try again.".to_owned(),
|
||||
})?;
|
||||
Ok((snapshot, Arc::new(results)))
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(format!("Navigation task failed: {error}")));
|
||||
nav::Completion { request, result }
|
||||
},
|
||||
Message::SourceSymbolsReady,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn source_symbols_ready(&mut self, completion: nav::Completion) -> Task<Message> {
|
||||
let request = completion.request;
|
||||
if self.source_symbols.in_flight != Some(request.job) {
|
||||
return Task::none();
|
||||
}
|
||||
self.source_symbols.in_flight = None;
|
||||
let diagram_query = self.source_diagram_job == Some(request.job);
|
||||
if diagram_query {
|
||||
self.source_diagram_job = None;
|
||||
}
|
||||
let wanted = self
|
||||
.source_symbols
|
||||
.request
|
||||
.take()
|
||||
.is_some_and(|current| current.job == request.job)
|
||||
&& request.interaction == self.source_symbols.interaction
|
||||
&& request.stamp.snapshot == self.source_key()
|
||||
&& nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
request.raw_interaction,
|
||||
)
|
||||
&& request.path == self.source_workspace.active
|
||||
&& request.cursor == self.source_editor.cursor()
|
||||
&& self.source_navigation_available();
|
||||
if !wanted {
|
||||
return Task::none();
|
||||
}
|
||||
match completion.result {
|
||||
Ok((snapshot, results)) if self.source_snapshot_matches(&snapshot) => {
|
||||
self.source_symbols.cache = Some(snapshot);
|
||||
self.source_symbols.results = Some(results.clone());
|
||||
if diagram_query {
|
||||
if results.disposition == query::Disposition::Direct
|
||||
&& results.scope.validated
|
||||
&& results.omitted_definitions == 0
|
||||
&& let [row] = results.definitions.as_slice()
|
||||
&& let query::Identity::Unique(identity) = row.declaration.identity
|
||||
&& row.membership == query::Membership::Included
|
||||
&& row.evidence == query::Evidence::Validated
|
||||
&& self.project().contains(identity.id())
|
||||
&& crate::diagram_usage::alias(self.project(), identity.id())
|
||||
== row.declaration.alias
|
||||
{
|
||||
self.source_symbols.open = false;
|
||||
return self.diagram_usage_event(crate::diagram_usage::Event::Open(
|
||||
crate::diagram_usage::Context {
|
||||
document: self.canvas_document,
|
||||
revision: self.document.revision(),
|
||||
subject: identity.id(),
|
||||
},
|
||||
));
|
||||
}
|
||||
self.source_symbols.notice = Some("Place the caret on a uniquely resolved alias in applied source to find diagram representations.".into());
|
||||
return Task::none();
|
||||
}
|
||||
if results.disposition == query::Disposition::Direct
|
||||
&& let Some(row) = results.definitions.first()
|
||||
{
|
||||
self.source_symbol_jump(&row.path, row.position, row.end_position);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
self.source_symbols.notice = Some("Source changed. Run navigation again.".into())
|
||||
}
|
||||
Err(error) => self.source_symbols.notice = Some(error),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn source_symbol_jump(&mut self, path: &str, start: query::Position, end: query::Position) {
|
||||
let cursor = text_editor::Cursor {
|
||||
position: text_editor::Position {
|
||||
line: start.line,
|
||||
column: start.byte_column,
|
||||
},
|
||||
selection: Some(text_editor::Position {
|
||||
line: end.line,
|
||||
column: end.byte_column,
|
||||
}),
|
||||
};
|
||||
if self
|
||||
.source_workspace
|
||||
.files
|
||||
.get(path)
|
||||
.is_none_or(|buffer| !nav::exact_cursor(&buffer.text, cursor))
|
||||
{
|
||||
self.source_symbols.notice =
|
||||
Some("That source location is no longer available. Run navigation again.".into());
|
||||
return;
|
||||
}
|
||||
if let Some(buffer) = self
|
||||
.source_workspace
|
||||
.files
|
||||
.get(&self.source_workspace.active)
|
||||
{
|
||||
let origin = nav::HistoryEntry {
|
||||
session: self.source_workspace.session(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
generation: buffer.generation(),
|
||||
cursor: self.source_editor.cursor(),
|
||||
};
|
||||
self.source_symbols
|
||||
.back
|
||||
.retain(|entry| entry.session == origin.session);
|
||||
if self.source_symbols.back.len() == 64 {
|
||||
self.source_symbols.back.remove(0);
|
||||
}
|
||||
self.source_symbols.back.push(origin);
|
||||
}
|
||||
self.source_workspace
|
||||
.switch(path, self.source_editor.cursor());
|
||||
self.show_source_file(false);
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
|
||||
fn source_symbol_back(&mut self) {
|
||||
while let Some(origin) = self.source_symbols.back.pop() {
|
||||
if origin.session != self.source_workspace.session() {
|
||||
continue;
|
||||
}
|
||||
let Some(buffer) = self.source_workspace.files.get(&origin.path) else {
|
||||
self.source_symbols.notice = Some("The previous file is no longer loaded.".into());
|
||||
return;
|
||||
};
|
||||
let unchanged = origin.generation == buffer.generation();
|
||||
let cursor = if unchanged {
|
||||
origin.cursor
|
||||
} else if origin.path == self.source_workspace.active {
|
||||
self.source_editor.cursor()
|
||||
} else {
|
||||
buffer.cursor
|
||||
};
|
||||
if !nav::exact_cursor(&buffer.text, cursor) {
|
||||
self.source_symbols.notice = Some(
|
||||
"The previous location changed. Open the file to choose a current position."
|
||||
.into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.source_workspace
|
||||
.switch(&origin.path, self.source_editor.cursor());
|
||||
self.show_source_file(false);
|
||||
self.queue_source_reveal(cursor, false);
|
||||
self.source_symbols.notice =
|
||||
(!unchanged).then(|| "This draft changed; its current cursor was restored.".into());
|
||||
return;
|
||||
}
|
||||
self.source_symbols.notice = Some("No previous source location in this workspace.".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1117,6 +1117,14 @@ fn in_machine(p: &Project, id: Id, machine: Id) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../harness/experiments/behavior-layout-scene-20260912/mod.rs"]
|
||||
mod behavior_layout_scene_experiment;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../harness/experiments/behavior-anchor-plan-20260912/mod.rs"]
|
||||
mod behavior_anchor_plan_experiment;
|
||||
|
||||
struct BoxItem {
|
||||
id: Id,
|
||||
rect: Rectangle,
|
||||
@@ -1156,6 +1164,17 @@ struct Line {
|
||||
label_at: Point,
|
||||
dashed: bool,
|
||||
}
|
||||
impl Line {
|
||||
fn label_bounds(&self) -> Rectangle {
|
||||
Rectangle::new(
|
||||
self.label_at,
|
||||
Size::new(
|
||||
(self.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||
25.,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
struct Scene {
|
||||
boxes: Vec<BoxItem>,
|
||||
lines: Vec<Line>,
|
||||
@@ -1317,6 +1336,8 @@ impl Scene {
|
||||
let mut y = if scenario { 148. } else { 120. };
|
||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
#[cfg(test)]
|
||||
behavior_anchor_plan_experiment::scene_extent(&mut scene);
|
||||
if scenario {
|
||||
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
||||
p,
|
||||
@@ -1424,6 +1445,10 @@ impl Scene {
|
||||
// the preceding action/state/reference box, before recording rows.
|
||||
*y += 16.;
|
||||
}
|
||||
#[cfg(test)]
|
||||
behavior_layout_scene_experiment::resolve_row_anchor(*id, y);
|
||||
#[cfg(test)]
|
||||
behavior_anchor_plan_experiment::row_anchor(*id, y);
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
@@ -1448,6 +1473,8 @@ impl Scene {
|
||||
(*y - top).max(95.),
|
||||
),
|
||||
);
|
||||
#[cfg(test)]
|
||||
let rect = behavior_anchor_plan_experiment::frame_bounds(*id, rect);
|
||||
self.boxes.insert(
|
||||
0,
|
||||
BoxItem {
|
||||
@@ -1632,6 +1659,8 @@ impl Scene {
|
||||
}
|
||||
if ordered {
|
||||
self.sequence_bounds.insert(owner, (sequence_top, *y - 42.));
|
||||
#[cfg(test)]
|
||||
behavior_anchor_plan_experiment::scope_end(owner, y, &mut self.sequence_bounds);
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
before: None,
|
||||
@@ -1831,17 +1860,7 @@ impl Scene {
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
.find(|line| {
|
||||
!line.label.is_empty()
|
||||
&& Rectangle::new(
|
||||
line.label_at,
|
||||
Size::new(
|
||||
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||
25.,
|
||||
),
|
||||
)
|
||||
.contains(point)
|
||||
})
|
||||
.find(|line| !line.label.is_empty() && line.label_bounds().contains(point))
|
||||
.map(|line| line.id)
|
||||
})
|
||||
.or_else(|| self.near_sequence_item(point))
|
||||
|
||||
@@ -17,6 +17,7 @@ pub fn keyboard_enabled(app: &crate::app::App) -> bool {
|
||||
&& !app.delete_pending
|
||||
&& app.pending.is_none()
|
||||
&& app.model_menu.is_none()
|
||||
&& app.diagram_usages.is_none()
|
||||
&& !app.reference_input_active()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::*;
|
||||
use iced::widget::{Column, column, text_input};
|
||||
use std::{cell::RefCell, collections::BTreeSet};
|
||||
pub(crate) mod focus;
|
||||
mod usage;
|
||||
mod workspace;
|
||||
pub(crate) use workspace::NAME_INPUT;
|
||||
pub use workspace::{Shelf, view};
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Navigation is derived from the same opaque-reference scene that is painted.
|
||||
use super::*;
|
||||
use crate::diagram_usage::{Surface, Target, Usage};
|
||||
|
||||
/// The renderer collapses whitespace before shortening. Require at least one
|
||||
/// actual character of this field, rather than an ellipsis at its start.
|
||||
fn label_field_visible(full: &str, prefix: &str, value: &str) -> bool {
|
||||
if value.trim().is_empty() {
|
||||
return false;
|
||||
}
|
||||
let normalized = full.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let normalized_prefix = prefix.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let start = normalized_prefix.len()
|
||||
+ usize::from(prefix.ends_with(char::is_whitespace) && !normalized_prefix.is_empty());
|
||||
let painted = crate::views::short(full, 32);
|
||||
let retained = normalized
|
||||
.chars()
|
||||
.zip(painted.chars())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(c, _)| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
start < retained
|
||||
}
|
||||
|
||||
fn usages(p: &Project, owner: Id, subject: Id, scene: &Scene) -> Vec<Usage> {
|
||||
let mut result = Vec::new();
|
||||
let mut add = |id, bounds, role: &str| {
|
||||
// Activation bars share the occurrence identity with a labelled message;
|
||||
// they must not inflate its target count.
|
||||
if !result
|
||||
.iter()
|
||||
.any(|u: &Usage| u.target == Target::ScenarioObject(id))
|
||||
{
|
||||
result.push(Usage {
|
||||
subject,
|
||||
surface: Surface::Scenario(owner),
|
||||
target: Target::ScenarioObject(id),
|
||||
role: role.into(),
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
};
|
||||
for item in scene.boxes.iter().filter(|item| !item.title.is_empty()) {
|
||||
let Some(element) = p.elements.get(&item.id) else {
|
||||
continue;
|
||||
};
|
||||
if subject == item.id {
|
||||
// A fragment's selectable header is useful even when its frame is
|
||||
// much larger than the viewport.
|
||||
let mut bounds = item.rect;
|
||||
if item.marker == Some(Kind::Occurrence) {
|
||||
bounds.height = bounds.height.min(38.);
|
||||
}
|
||||
add(item.id, bounds, "scenario object");
|
||||
}
|
||||
let represented = match element.shared() {
|
||||
Some(SharedBody::Lifeline { represented }) => represented.map(|id| (id, "lifeline")),
|
||||
Some(SharedBody::Occurrence { body, .. }) => match body {
|
||||
OccurrenceBody::Action { action, .. } => action.map(|id| (id, "action occurrence")),
|
||||
OccurrenceBody::State { state, .. } => state.map(|id| (id, "state observation")),
|
||||
OccurrenceBody::SequenceReference { sequence } => {
|
||||
sequence.map(|id| (id, "process reference"))
|
||||
}
|
||||
OccurrenceBody::ScenarioUse { scenario, .. } => {
|
||||
scenario.map(|id| (id, "scenario reference"))
|
||||
}
|
||||
// An incomplete message fallback box only paints the occurrence
|
||||
// name and a repair hint, not its exchange or operation.
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
if let Some((id, role)) = represented.filter(|(id, _)| *id == subject) {
|
||||
let _ = id;
|
||||
add(item.id, item.rect, role);
|
||||
}
|
||||
}
|
||||
for line in scene.lines.iter().filter(|line| !line.label.is_empty()) {
|
||||
let Some(element) = p.elements.get(&line.id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = element.shared()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let bounds = line.label_bounds();
|
||||
if subject == line.id || *exchange == Some(subject) {
|
||||
add(line.id, bounds, "message occurrence");
|
||||
}
|
||||
// Replies derive their operation from the exact call, matching the
|
||||
// operation label formatter rather than scanning raw reference fields.
|
||||
if let Ok(operations::OperationResolution::Selected(operation)) =
|
||||
operations::resolve(p, line.id)
|
||||
&& let Some(label) = crate::data_values::operation_label(p, element)
|
||||
&& let Some(prefix) = line.label.strip_suffix(&label)
|
||||
{
|
||||
let allocation = &p.relations[&operation.allocation];
|
||||
let operation_prefix = if matches!(
|
||||
element.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
) {
|
||||
"Reply operation from Call: "
|
||||
} else {
|
||||
"Operation: "
|
||||
};
|
||||
let fields = [
|
||||
(allocation.source(), format!("{prefix}{operation_prefix}")),
|
||||
(
|
||||
allocation.target(),
|
||||
format!(
|
||||
"{prefix}{operation_prefix}{} / ",
|
||||
p.named(allocation.source())
|
||||
),
|
||||
),
|
||||
(
|
||||
allocation.id,
|
||||
format!(
|
||||
"{prefix}{operation_prefix}{} / {} · ",
|
||||
p.named(allocation.source()),
|
||||
p.named(allocation.target())
|
||||
),
|
||||
),
|
||||
];
|
||||
for (id, prefix) in fields {
|
||||
if id == subject && label_field_visible(&line.label, &prefix, p.named(id)) {
|
||||
add(line.id, bounds, "operation label");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if subject == owner {
|
||||
result.insert(
|
||||
0,
|
||||
Usage {
|
||||
subject,
|
||||
surface: Surface::Scenario(owner),
|
||||
target: Target::Whole,
|
||||
role: "scenario".into(),
|
||||
bounds: Rectangle::new(Point::ORIGIN, scene.size),
|
||||
},
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn diagram_usages(&self, p: &Project, owner: Id, subject: Id) -> Vec<Usage> {
|
||||
if !p
|
||||
.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|e| e.kind() == Kind::Scenario)
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
usages(p, owner, subject, &Scene::sequence(p, owner, subject))
|
||||
}
|
||||
|
||||
pub(crate) fn reveal_usage(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
document: Id,
|
||||
owner: Id,
|
||||
target: &Target,
|
||||
) -> bool {
|
||||
if self.draft.is_some()
|
||||
|| !p
|
||||
.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|e| e.kind() == Kind::Scenario)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let id = match target {
|
||||
Target::Whole => owner,
|
||||
Target::ScenarioObject(id) => *id,
|
||||
_ => return false,
|
||||
};
|
||||
let Some(usage) = self
|
||||
.diagram_usages(p, owner, id)
|
||||
.into_iter()
|
||||
.find(|u| &u.target == target)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
// Preserve an existing mounted viewport across enter's owner reset.
|
||||
let viewport = self.presentation.viewport.unwrap_or(Size::new(500., 300.));
|
||||
self.enter(document, owner);
|
||||
self.presentation.viewport = Some(viewport);
|
||||
self.canvas_tool = Some((owner, CanvasTool::Select));
|
||||
let (zoom, pan) = crate::diagram_usage::camera_for(target, usage.bounds, viewport);
|
||||
self.presentation.zoom(Some(zoom));
|
||||
self.presentation.pan = pan;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,276 @@
|
||||
use super::*;
|
||||
|
||||
fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let layer = p.elements[&owner].layer();
|
||||
let id = p
|
||||
.add_element(Kind::Occurrence, layer, name, Some(owner))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() {
|
||||
if !steps.contains(&id) {
|
||||
steps.push(id);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
fn targets(state: &State, p: &Project, owner: Id, subject: Id) -> BTreeSet<Id> {
|
||||
state
|
||||
.diagram_usages(p, owner, subject)
|
||||
.into_iter()
|
||||
.filter_map(|u| match u.target {
|
||||
Target::ScenarioObject(id) => Some(id),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_usage_counts_nested_occurrences_and_lifelines_but_keeps_reuse_opaque() {
|
||||
for (layer, work_kind, participant_kind) in [
|
||||
(Layer::Oa, Kind::Activity, Kind::Entity),
|
||||
(Layer::Sa, Kind::Function, Kind::SystemActor),
|
||||
] {
|
||||
let mut p = Project::empty();
|
||||
let owner = p
|
||||
.add_element(Kind::Scenario, layer, "Caller", None)
|
||||
.unwrap();
|
||||
let other = p
|
||||
.add_element(Kind::Scenario, layer, "Definition", None)
|
||||
.unwrap();
|
||||
let work = p.add_element(work_kind, layer, "Respond", None).unwrap();
|
||||
let unused = p.add_element(work_kind, layer, "Respond", None).unwrap();
|
||||
let participant = p
|
||||
.add_element(participant_kind, layer, "Operator", None)
|
||||
.unwrap();
|
||||
let lanes = ["First role", "Second role"].map(|name| {
|
||||
let id = p
|
||||
.add_element(Kind::Lifeline, layer, name, Some(owner))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(participant),
|
||||
};
|
||||
id
|
||||
});
|
||||
let first = add_occurrence(
|
||||
&mut p,
|
||||
owner,
|
||||
"First action",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: Some(lanes[0]),
|
||||
},
|
||||
);
|
||||
let fragment = add_occurrence(
|
||||
&mut p,
|
||||
owner,
|
||||
"Decision",
|
||||
OccurrenceBody::Fragment {
|
||||
operator: FragmentOperator::Alternative,
|
||||
branches: vec![],
|
||||
},
|
||||
);
|
||||
let branch = add_occurrence(
|
||||
&mut p,
|
||||
fragment,
|
||||
"When ready",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "ready".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
||||
{
|
||||
branches.push(branch);
|
||||
}
|
||||
let nested = add_occurrence(
|
||||
&mut p,
|
||||
branch,
|
||||
"Again",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: Some(lanes[1]),
|
||||
},
|
||||
);
|
||||
let inside = add_occurrence(
|
||||
&mut p,
|
||||
other,
|
||||
"Hidden inside definition",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(unused),
|
||||
lifeline: None,
|
||||
},
|
||||
);
|
||||
let use_a = add_occurrence(
|
||||
&mut p,
|
||||
owner,
|
||||
"Use A",
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(other),
|
||||
bindings: vec![],
|
||||
},
|
||||
);
|
||||
let use_b = add_occurrence(
|
||||
&mut p,
|
||||
owner,
|
||||
"Use B",
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(other),
|
||||
bindings: vec![],
|
||||
},
|
||||
);
|
||||
let before = p.clone();
|
||||
let state = State::default();
|
||||
assert_eq!(
|
||||
targets(&state, &p, owner, work),
|
||||
BTreeSet::from([first, nested])
|
||||
);
|
||||
assert_eq!(
|
||||
targets(&state, &p, owner, participant),
|
||||
BTreeSet::from(lanes)
|
||||
);
|
||||
assert_eq!(
|
||||
targets(&state, &p, owner, other),
|
||||
BTreeSet::from([use_a, use_b])
|
||||
);
|
||||
assert!(targets(&state, &p, owner, unused).is_empty());
|
||||
assert!(targets(&state, &p, owner, inside).is_empty());
|
||||
assert_eq!(targets(&state, &p, owner, branch), BTreeSet::from([branch]));
|
||||
assert_eq!(
|
||||
state.diagram_usages(&p, owner, owner)[0].target,
|
||||
Target::Whole
|
||||
);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_usage_keeps_repeated_messages_and_reveal_is_read_only_and_revalidated() {
|
||||
let f = super::super::direct_tests::fixture();
|
||||
let mut p = f.project;
|
||||
let mut state = State::default();
|
||||
assert_eq!(
|
||||
targets(&state, &p, f.owner, f.exchange),
|
||||
BTreeSet::from([f.call, f.reply])
|
||||
);
|
||||
let before = p.clone();
|
||||
let target = Target::ScenarioObject(f.reply);
|
||||
state.viewport(Size::new(900., 500.));
|
||||
assert!(state.reveal_usage(&p, Id::new_v4(), f.owner, &target));
|
||||
let usage = state
|
||||
.diagram_usages(&p, f.owner, f.reply)
|
||||
.into_iter()
|
||||
.find(|u| u.target == target)
|
||||
.unwrap();
|
||||
let zoom = state
|
||||
.presentation
|
||||
.scale(Scene::sequence(&p, f.owner, f.reply).size);
|
||||
let centered = Point::new(
|
||||
usage.bounds.center_x() * zoom,
|
||||
usage.bounds.center_y() * zoom,
|
||||
) + state.presentation.pan;
|
||||
assert!((centered.x - 450.).abs() < 0.01 && (centered.y - 250.).abs() < 0.01);
|
||||
assert!(zoom >= 0.5);
|
||||
assert_eq!(p, before);
|
||||
// Keeping an orphan semantic occurrence does not keep its painted target.
|
||||
p.elements
|
||||
.get_mut(&f.owner)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.retain(|id| *id != f.reply);
|
||||
let camera = state.camera_boundary();
|
||||
assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &target));
|
||||
assert!(camera == state.camera_boundary());
|
||||
assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &Target::Edge(f.exchange)));
|
||||
// A message whose endpoints are incomplete paints only its repair box.
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { receiver, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&f.call).unwrap().shared_mut()
|
||||
{
|
||||
*receiver = None;
|
||||
}
|
||||
assert!(targets(&state, &p, f.owner, f.exchange).is_empty());
|
||||
assert_eq!(
|
||||
targets(&state, &p, f.owner, f.call),
|
||||
BTreeSet::from([f.call])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_operation_label_membership_excludes_ellipsis_only_and_normalizes_whitespace() {
|
||||
assert!(label_field_visible(
|
||||
"1. call · X · Operation: I / O",
|
||||
"1. call · X · Operation: ",
|
||||
"I"
|
||||
));
|
||||
assert!(!label_field_visible(
|
||||
"1234567890123456789012345678901Ixyz",
|
||||
"1234567890123456789012345678901",
|
||||
"Ixyz"
|
||||
));
|
||||
assert!(!label_field_visible(
|
||||
"A very long occurrence label · Operation: I",
|
||||
"A very long occurrence label · Operation: ",
|
||||
"I"
|
||||
));
|
||||
assert!(label_field_visible(
|
||||
"call\n\t· Operation: 界",
|
||||
"call\n\t· Operation: ",
|
||||
"界"
|
||||
));
|
||||
assert!(!label_field_visible("Operation: ", "Operation: ", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_usage_only_reports_the_resolved_operation_fields_actually_in_the_label() {
|
||||
let mut p = persistence::decode(include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
)))
|
||||
.unwrap();
|
||||
let id = |n| Id::parse_str(&format!("52060906-0000-4000-8000-{n:012x}")).unwrap();
|
||||
let (owner, call, exchange, allocation) = (id(40), id(43), id(34), id(30));
|
||||
let (interface, item) = (
|
||||
p.relations[&allocation].source(),
|
||||
p.relations[&allocation].target(),
|
||||
);
|
||||
for (id, name) in [(call, "x"), (interface, "I"), (item, "O")] {
|
||||
p.elements.get_mut(&id).unwrap().name = name.into();
|
||||
}
|
||||
p.relations.get_mut(&exchange).unwrap().name = "x".into();
|
||||
p.relations.get_mut(&allocation).unwrap().name = "A".into();
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { operation, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
||||
{
|
||||
*operation = Some(OperationUse {
|
||||
allocation: Some(allocation),
|
||||
});
|
||||
}
|
||||
assert!(matches!(
|
||||
operations::resolve(&p, call),
|
||||
Ok(operations::OperationResolution::Selected(_))
|
||||
));
|
||||
let state = State::default();
|
||||
let before = p.clone();
|
||||
assert_eq!(
|
||||
targets(&state, &p, owner, interface),
|
||||
BTreeSet::from([call])
|
||||
);
|
||||
assert_eq!(targets(&state, &p, owner, item), BTreeSet::from([call]));
|
||||
assert!(targets(&state, &p, owner, allocation).is_empty());
|
||||
assert_eq!(p, before);
|
||||
p.elements.get_mut(&call).unwrap().name = "This occurrence has a deliberately long name".into();
|
||||
assert!(targets(&state, &p, owner, interface).is_empty());
|
||||
assert!(targets(&state, &p, owner, item).is_empty());
|
||||
}
|
||||
@@ -27,6 +27,8 @@ pub struct StateCanvas<'a> {
|
||||
pub diagram: Id,
|
||||
pub context: Context,
|
||||
pub selected: Id,
|
||||
/// Presentation-only navigation marker; never an authoring placement.
|
||||
pub navigation_focus: Option<Rectangle>,
|
||||
pub tool: Tool,
|
||||
pub zoom: f32,
|
||||
pub pan: Vector,
|
||||
@@ -415,6 +417,171 @@ fn state_action_label(project: &Project, relation: &Relation) -> String {
|
||||
format!("{phase} / {}", project.named(target))
|
||||
}
|
||||
|
||||
/// Match a typed label field by its structural offset, never by a display-name
|
||||
/// substring (names may duplicate or also occur in free-form guards).
|
||||
fn retained_label_field(full: &str, painted: &str, start: usize) -> bool {
|
||||
start
|
||||
< full
|
||||
.chars()
|
||||
.zip(painted.chars())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(c, _)| c.len_utf8())
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
fn transition_label_role(
|
||||
project: &Project,
|
||||
relation: &Relation,
|
||||
subject: Id,
|
||||
width: f32,
|
||||
) -> Option<&'static str> {
|
||||
let full = routing::state_transition_summary(project, relation);
|
||||
transition_label_role_in_text(project, relation, subject, &short(&full, width, 10.))
|
||||
}
|
||||
|
||||
fn transition_label_role_in_text(
|
||||
project: &Project,
|
||||
relation: &Relation,
|
||||
subject: Id,
|
||||
painted: &str,
|
||||
) -> Option<&'static str> {
|
||||
let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
triggers,
|
||||
effects,
|
||||
effect,
|
||||
..
|
||||
} = &relation.body
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if project.named(subject).trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let full = routing::state_transition_summary(project, relation);
|
||||
let mut offset = format!("{transition_kind:?} · ").len();
|
||||
for id in triggers {
|
||||
if *id == subject && retained_label_field(&full, painted, offset) {
|
||||
return Some("transition trigger");
|
||||
}
|
||||
offset += project.named(*id).len() + " OR ".len();
|
||||
}
|
||||
if !effects.is_empty() {
|
||||
// The canonical routing summary appends typed effects before optional
|
||||
// authored effect prose. Derive its offset from that exact suffix.
|
||||
let mut suffix = format!(
|
||||
" / {}",
|
||||
effects
|
||||
.iter()
|
||||
.map(|id| project.named(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
if !effect.is_empty() {
|
||||
suffix.push_str(&format!(" / {effect}"));
|
||||
}
|
||||
if let Some(prefix) = full.strip_suffix(&suffix) {
|
||||
offset = prefix.len() + " / ".len();
|
||||
for id in effects {
|
||||
if *id == subject && retained_label_field(&full, painted, offset) {
|
||||
return Some("transition effect");
|
||||
}
|
||||
offset += project.named(*id).len() + ", ".len();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Actual state compartments and painted transition-reference labels. Generic
|
||||
/// projected node/edge identities are handled by the shared saved-view query.
|
||||
pub(crate) fn diagram_usages(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
view: &ProjectedView,
|
||||
subject: Id,
|
||||
) -> Vec<crate::diagram_usage::Usage> {
|
||||
use crate::diagram_usage::{Surface, Target, Usage};
|
||||
if diagram.projection.state_machine().is_none() {
|
||||
return vec![];
|
||||
}
|
||||
let mut result = Vec::new();
|
||||
for node in &view.nodes {
|
||||
for (relation, bounds) in state_action_rows(project, node) {
|
||||
let label = state_action_label(project, relation);
|
||||
let painted = short(&label, bounds.width - 12., 11.);
|
||||
let role = if relation.id == subject {
|
||||
Some("state action")
|
||||
} else if relation.target() == subject
|
||||
&& retained_label_field(
|
||||
&label,
|
||||
&painted,
|
||||
label.len() - project.named(subject).len(),
|
||||
)
|
||||
{
|
||||
Some("state action work")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(role) = role {
|
||||
result.push(Usage {
|
||||
subject,
|
||||
surface: Surface::Saved(diagram.id),
|
||||
target: Target::StateRow {
|
||||
placement: node.id,
|
||||
relation: relation.id,
|
||||
},
|
||||
role: role.into(),
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (relation, bounds) in internal_rows(project, node) {
|
||||
let role = if relation.id == subject {
|
||||
Some("internal transition")
|
||||
} else {
|
||||
transition_label_role(project, relation, subject, bounds.width - 12.)
|
||||
};
|
||||
if let Some(role) = role {
|
||||
result.push(Usage {
|
||||
subject,
|
||||
surface: Surface::Saved(diagram.id),
|
||||
target: Target::StateRow {
|
||||
placement: node.id,
|
||||
relation: relation.id,
|
||||
},
|
||||
role: role.into(),
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in routing::route_diagram(project, diagram, view)
|
||||
.edges
|
||||
.values()
|
||||
{
|
||||
let Some(relation) = project.relations.get(&edge.id) else {
|
||||
continue;
|
||||
};
|
||||
if edge.id == subject || edge.label.text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Routing already shortens external summaries before the canvas applies
|
||||
// its width limit. Compare against precisely that doubly shortened text.
|
||||
let painted = short(&edge.label.text, edge.label.bounds.size.width - 10., 10.);
|
||||
if let Some(role) = transition_label_role_in_text(project, relation, subject, &painted) {
|
||||
result.push(Usage {
|
||||
subject,
|
||||
surface: Surface::Saved(diagram.id),
|
||||
target: Target::Edge(edge.id),
|
||||
role: role.into(),
|
||||
bounds: crate::diagram_usage::edge_focus_bounds(edge),
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl StateCanvas<'_> {
|
||||
fn scale(&self) -> f32 {
|
||||
if self.zoom.is_finite() {
|
||||
@@ -1148,6 +1315,14 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
overlay.with_clip(content, |frame| {
|
||||
frame.translate(self.offset());
|
||||
frame.scale(self.scale());
|
||||
if let Some(bounds) = self.navigation_focus {
|
||||
frame.stroke(
|
||||
&Path::rectangle(bounds.position(), bounds.size()),
|
||||
Stroke::default()
|
||||
.with_color(style::TEAL)
|
||||
.with_width(2.5 / self.scale()),
|
||||
);
|
||||
}
|
||||
if let Some(at) = world
|
||||
&& self.editable
|
||||
{
|
||||
@@ -1575,6 +1750,189 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_usage_state_rows_and_label_references_follow_actual_projection() {
|
||||
use crate::diagram_usage::Target;
|
||||
let mut f = fixture();
|
||||
let work = f
|
||||
.project
|
||||
.add_element(Kind::Function, Layer::Sa, "Act", None)
|
||||
.unwrap();
|
||||
let event = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, "E", None)
|
||||
.unwrap();
|
||||
let unused = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, "E", None)
|
||||
.unwrap();
|
||||
let action = f
|
||||
.project
|
||||
.relate(RelationKind::StateAction, "Entry", f.a, work);
|
||||
let internal = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "Internal", f.a, f.a);
|
||||
let external = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "External", f.a, f.b);
|
||||
for (id, kind) in [
|
||||
(internal, TransitionKind::Internal),
|
||||
(external, TransitionKind::External),
|
||||
] {
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
triggers,
|
||||
effects,
|
||||
..
|
||||
} = &mut f.project.relations.get_mut(&id).unwrap().body
|
||||
{
|
||||
*transition_kind = kind;
|
||||
*triggers = vec![event];
|
||||
*effects = vec![work];
|
||||
}
|
||||
}
|
||||
let before = f.project.clone();
|
||||
let diagram = &f.project.diagrams[&f.diagram];
|
||||
let view = projection::project_diagram(&f.project, diagram);
|
||||
let placement = view.nodes.iter().find(|n| n.element == f.a).unwrap().id;
|
||||
let action_target = Target::StateRow {
|
||||
placement,
|
||||
relation: action,
|
||||
};
|
||||
let internal_target = Target::StateRow {
|
||||
placement,
|
||||
relation: internal,
|
||||
};
|
||||
assert_eq!(
|
||||
diagram_usages(&f.project, diagram, &view, action)[0].target,
|
||||
action_target
|
||||
);
|
||||
assert_eq!(
|
||||
diagram_usages(&f.project, diagram, &view, internal)[0].target,
|
||||
internal_target
|
||||
);
|
||||
let events = diagram_usages(&f.project, diagram, &view, event);
|
||||
assert_eq!(events.len(), 2);
|
||||
assert!(events.iter().any(|u| u.target == internal_target));
|
||||
assert!(events.iter().any(|u| u.target == Target::Edge(external)));
|
||||
assert!(diagram_usages(&f.project, diagram, &view, unused).is_empty());
|
||||
// External edge identity belongs to the shared saved-view adapter.
|
||||
assert!(diagram_usages(&f.project, diagram, &view, external).is_empty());
|
||||
let work_usages = diagram_usages(&f.project, diagram, &view, work);
|
||||
assert!(work_usages.iter().any(|u| u.target == action_target));
|
||||
assert!(work_usages.iter().any(|u| u.target == internal_target));
|
||||
let row = work_usages
|
||||
.iter()
|
||||
.find(|u| u.target == action_target)
|
||||
.unwrap();
|
||||
let canvas = canvas(&f, Tool::Select, 1.);
|
||||
let scene = canvas.scene(&State::default()).unwrap();
|
||||
assert_eq!(
|
||||
canvas.hit(&scene, row.bounds.center()),
|
||||
Some(Hit::StateAction(action))
|
||||
);
|
||||
assert_eq!(f.project, before);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&placement)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let diagram = &f.project.diagrams[&f.diagram];
|
||||
let folded = projection::project_diagram(&f.project, diagram);
|
||||
assert!(diagram_usages(&f.project, diagram, &folded, action).is_empty());
|
||||
assert!(diagram_usages(&f.project, diagram, &folded, internal).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_usage_transition_labels_do_not_match_prose_or_fields_beyond_cutoff() {
|
||||
let mut f = fixture();
|
||||
let early = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, "A", None)
|
||||
.unwrap();
|
||||
let late = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, "B", None)
|
||||
.unwrap();
|
||||
let relation = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "Transition", f.a, f.b);
|
||||
if let RelationBody::StateTransition {
|
||||
triggers, guard, ..
|
||||
} = &mut f.project.relations.get_mut(&relation).unwrap().body
|
||||
{
|
||||
*triggers = vec![early, late];
|
||||
*guard = "B".into();
|
||||
}
|
||||
let relation = &f.project.relations[&relation];
|
||||
assert_eq!(
|
||||
transition_label_role(&f.project, relation, early, 84.),
|
||||
Some("transition trigger")
|
||||
);
|
||||
assert_eq!(transition_label_role(&f.project, relation, late, 84.), None);
|
||||
assert_eq!(
|
||||
transition_label_role(&f.project, relation, late, 800.),
|
||||
Some("transition trigger")
|
||||
);
|
||||
assert!(!retained_label_field("1234A678", "1234…", 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_usage_external_labels_obey_routing_cutoff_while_internal_rows_retain_full_summary() {
|
||||
use crate::diagram_usage::Target;
|
||||
let mut f = fixture();
|
||||
let first = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, &"A".repeat(35), None)
|
||||
.unwrap();
|
||||
let second = f
|
||||
.project
|
||||
.add_element(Kind::Event, Layer::Sa, "Second", None)
|
||||
.unwrap();
|
||||
let external = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "External", f.a, f.b);
|
||||
let internal = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "Internal", f.a, f.a);
|
||||
for (id, kind) in [
|
||||
(external, TransitionKind::External),
|
||||
(internal, TransitionKind::Internal),
|
||||
] {
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
triggers,
|
||||
..
|
||||
} = &mut f.project.relations.get_mut(&id).unwrap().body
|
||||
{
|
||||
*transition_kind = kind;
|
||||
*triggers = vec![first, second];
|
||||
}
|
||||
}
|
||||
let diagram = &f.project.diagrams[&f.diagram];
|
||||
let view = projection::project_diagram(&f.project, diagram);
|
||||
let routed = routing::route_diagram(&f.project, diagram, &view);
|
||||
let edge = &routed.edges[&external];
|
||||
let painted = short(&edge.label.text, edge.label.bounds.size.width - 10., 10.);
|
||||
// Independent painted-text oracle: 11 prefix characters + 35 As, then
|
||||
// only the space before OR survives the routing limit.
|
||||
assert_eq!(edge.label.text, format!("External · {} …", "A".repeat(35)));
|
||||
assert!(!painted.contains("Second"));
|
||||
let usages = diagram_usages(&f.project, diagram, &view, second);
|
||||
assert!(!usages.iter().any(|u| u.target == Target::Edge(external)));
|
||||
assert!(usages.iter().any(
|
||||
|u| matches!(u.target, Target::StateRow { relation, .. } if relation == internal)
|
||||
));
|
||||
assert!(
|
||||
diagram_usages(&f.project, diagram, &view, first)
|
||||
.iter()
|
||||
.any(|u| u.target == Target::Edge(external))
|
||||
);
|
||||
}
|
||||
|
||||
fn canvas(f: &Fixture, tool: Tool, zoom: f32) -> StateCanvas<'_> {
|
||||
StateCanvas {
|
||||
project: &f.project,
|
||||
@@ -1586,6 +1944,7 @@ mod tests {
|
||||
epoch: 3,
|
||||
},
|
||||
selected: f.machine,
|
||||
navigation_focus: None,
|
||||
tool,
|
||||
zoom,
|
||||
pan: Vector::new(20., 30.),
|
||||
|
||||
@@ -165,6 +165,9 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
|
||||
diagram: id,
|
||||
context,
|
||||
selected: app.selected,
|
||||
navigation_focus: app
|
||||
.diagram_focus(crate::diagram_usage::Surface::Saved(id))
|
||||
.map(|usage| usage.bounds),
|
||||
tool: state.tool,
|
||||
zoom: state.zoom,
|
||||
pan: state.pan,
|
||||
|
||||
@@ -5,6 +5,7 @@ use iced::widget::{Column, text_input};
|
||||
use std::collections::BTreeSet;
|
||||
pub(crate) mod contextual;
|
||||
mod direct;
|
||||
mod usages;
|
||||
mod view;
|
||||
use direct::Input;
|
||||
pub(crate) use direct::{
|
||||
@@ -910,6 +911,13 @@ fn sequence_links(p: &Project, owner: Id) -> Vec<&Relation> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PreviewLink {
|
||||
relation: Id,
|
||||
source: OccurrenceEndpoint,
|
||||
target: OccurrenceEndpoint,
|
||||
}
|
||||
|
||||
struct GraphNode {
|
||||
id: Id,
|
||||
rect: Rectangle,
|
||||
@@ -918,26 +926,98 @@ struct GraphNode {
|
||||
control: Option<ControlKind>,
|
||||
source: Option<Id>,
|
||||
preview: Vec<(OccurrenceEndpoint, String)>,
|
||||
preview_links: Vec<(OccurrenceEndpoint, OccurrenceEndpoint)>,
|
||||
preview_links: Vec<PreviewLink>,
|
||||
preview_total: usize,
|
||||
}
|
||||
impl GraphNode {
|
||||
fn preview_link_points(&self, link: &PreviewLink) -> Option<[Point; 2]> {
|
||||
if ![&link.source, &link.target]
|
||||
.into_iter()
|
||||
.all(|endpoint| self.preview.iter().any(|(shown, _)| shown == endpoint))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let a = self.endpoint_rect(&link.source);
|
||||
let b = self.endpoint_rect(&link.target);
|
||||
Some([
|
||||
Point::new(a.x + a.width, a.center_y()),
|
||||
Point::new(b.x, b.center_y()),
|
||||
])
|
||||
}
|
||||
|
||||
fn preview_link_visible_segment(&self, link: &PreviewLink) -> Option<[Point; 2]> {
|
||||
visible_preview_segment(
|
||||
self.preview_link_points(link)?,
|
||||
(0..self.preview.len()).map(|index| self.preview_rect(index)),
|
||||
)
|
||||
}
|
||||
|
||||
fn preview_rect(&self, index: usize) -> Rectangle {
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
self.rect.x + 12. + (index % 2) as f32 * 125.,
|
||||
self.rect.y + 118. + (index / 2) as f32 * 44.,
|
||||
),
|
||||
Size::new(113., 32.),
|
||||
)
|
||||
}
|
||||
|
||||
fn endpoint_rect(&self, endpoint: &OccurrenceEndpoint) -> Rectangle {
|
||||
self.preview
|
||||
.iter()
|
||||
.position(|(candidate, _)| candidate == endpoint)
|
||||
.map(|i| {
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
self.rect.x + 12. + (i % 2) as f32 * 125.,
|
||||
self.rect.y + 118. + (i / 2) as f32 * 44.,
|
||||
),
|
||||
Size::new(113., 32.),
|
||||
)
|
||||
})
|
||||
.map(|index| self.preview_rect(index))
|
||||
.unwrap_or(self.rect)
|
||||
}
|
||||
}
|
||||
|
||||
/// Preview tiles are painted after their arrows. Focus only a remaining line
|
||||
/// segment, excluding opaque tile interiors and their one-pixel borders.
|
||||
fn visible_preview_segment(
|
||||
[a, b]: [Point; 2],
|
||||
tiles: impl Iterator<Item = Rectangle>,
|
||||
) -> Option<[Point; 2]> {
|
||||
let delta = b - a;
|
||||
let length = (delta.x * delta.x + delta.y * delta.y).sqrt();
|
||||
if !length.is_finite() || length <= 0.5 {
|
||||
return None;
|
||||
}
|
||||
let mut hidden = Vec::new();
|
||||
for tile in tiles {
|
||||
let mut low = 0_f32;
|
||||
let mut high = 1_f32;
|
||||
for (origin, step, min, max) in [
|
||||
(a.x, delta.x, tile.x - 1., tile.x + tile.width + 1.),
|
||||
(a.y, delta.y, tile.y - 1., tile.y + tile.height + 1.),
|
||||
] {
|
||||
if step.abs() <= f32::EPSILON {
|
||||
if origin < min || origin > max {
|
||||
high = -1.;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
let first = (min - origin) / step;
|
||||
let last = (max - origin) / step;
|
||||
low = low.max(first.min(last));
|
||||
high = high.min(first.max(last));
|
||||
}
|
||||
}
|
||||
if high > low {
|
||||
hidden.push((low, high));
|
||||
}
|
||||
}
|
||||
hidden.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut cursor = 0_f32;
|
||||
let mut best = (0_f32, 0_f32);
|
||||
for (low, high) in hidden.into_iter().chain([(1., 1.)]) {
|
||||
if low - cursor > best.1 - best.0 {
|
||||
best = (cursor, low);
|
||||
}
|
||||
cursor = cursor.max(high);
|
||||
}
|
||||
((best.1 - best.0) * length > 0.5).then(|| [a + delta * best.0, a + delta * best.1])
|
||||
}
|
||||
|
||||
struct GraphEdge {
|
||||
id: Id,
|
||||
source: Id,
|
||||
@@ -986,6 +1066,8 @@ struct Graph<'a> {
|
||||
ordered: bool,
|
||||
size: Size,
|
||||
selected: Id,
|
||||
selected_preview: Option<OccurrenceEndpoint>,
|
||||
selected_preview_link: Option<PreviewLink>,
|
||||
scale: f32,
|
||||
}
|
||||
impl<'a> Graph<'a> {
|
||||
@@ -1134,17 +1216,16 @@ impl<'a> Graph<'a> {
|
||||
occurrence: r.target(),
|
||||
})
|
||||
})
|
||||
.map(|r| {
|
||||
(
|
||||
OccurrenceEndpoint {
|
||||
references: vec![e.id],
|
||||
occurrence: r.source(),
|
||||
},
|
||||
OccurrenceEndpoint {
|
||||
references: vec![e.id],
|
||||
occurrence: r.target(),
|
||||
},
|
||||
)
|
||||
.map(|r| PreviewLink {
|
||||
relation: r.id,
|
||||
source: OccurrenceEndpoint {
|
||||
references: vec![e.id],
|
||||
occurrence: r.source(),
|
||||
},
|
||||
target: OccurrenceEndpoint {
|
||||
references: vec![e.id],
|
||||
occurrence: r.target(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -1319,6 +1400,8 @@ impl<'a> Graph<'a> {
|
||||
ordered: !order.is_empty(),
|
||||
size,
|
||||
selected,
|
||||
selected_preview: None,
|
||||
selected_preview_link: None,
|
||||
scale: 1.,
|
||||
}
|
||||
}
|
||||
@@ -1548,39 +1631,41 @@ impl canvas::Program<Message> for Graph<'_> {
|
||||
let mini: BTreeMap<_, _> = n
|
||||
.preview
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (id, _))| {
|
||||
(
|
||||
id.clone(),
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
n.rect.x + 12. + (i % 2) as f32 * 125.,
|
||||
n.rect.y + 118. + (i / 2) as f32 * 44.,
|
||||
),
|
||||
Size::new(113., 32.),
|
||||
),
|
||||
)
|
||||
})
|
||||
.map(|(id, _)| (id.clone(), n.endpoint_rect(id)))
|
||||
.collect();
|
||||
for (a, b) in &n.preview_links {
|
||||
let a = mini[a];
|
||||
let b = mini[b];
|
||||
for link in &n.preview_links {
|
||||
let Some(points) = n.preview_link_points(link) else {
|
||||
continue;
|
||||
};
|
||||
let selected = self.selected_preview_link.as_ref() == Some(link);
|
||||
draw_arrow(
|
||||
&mut f,
|
||||
&[
|
||||
Point::new(a.x + a.width, a.center_y()),
|
||||
Point::new(b.x, b.center_y()),
|
||||
],
|
||||
style::MUTED,
|
||||
1.,
|
||||
&points,
|
||||
if selected { style::TEAL } else { style::MUTED },
|
||||
if selected { 3. } else { 1. },
|
||||
);
|
||||
}
|
||||
for (id, name) in &n.preview {
|
||||
let r = mini[id];
|
||||
f.fill_rectangle(r.position(), r.size(), Color::WHITE);
|
||||
let selected = self.selected_preview.as_ref() == Some(id);
|
||||
f.fill_rectangle(
|
||||
r.position(),
|
||||
r.size(),
|
||||
if selected {
|
||||
Color::from_rgb8(224, 247, 242)
|
||||
} else {
|
||||
Color::WHITE
|
||||
},
|
||||
);
|
||||
f.stroke(
|
||||
&Path::rectangle(r.position(), r.size()),
|
||||
Stroke::default().with_color(style::LINE),
|
||||
Stroke::default()
|
||||
.with_color(if selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::LINE
|
||||
})
|
||||
.with_width(if selected { 2.5 } else { 1. }),
|
||||
);
|
||||
draw_text(
|
||||
&mut f,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! Read-only usages of the actual process graph, including bounded reuse tiles.
|
||||
use super::*;
|
||||
use crate::diagram_usage::{Surface, Target, Usage};
|
||||
|
||||
/// The only presentation inputs needed by a background usage query. Drafts,
|
||||
/// focus, camera, history and widget state never cross the worker boundary.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct UsageSnapshot {
|
||||
identity: Option<(Id, Id)>,
|
||||
expanded: BTreeSet<Id>,
|
||||
contextual_expanded: BTreeSet<OccurrenceEndpoint>,
|
||||
}
|
||||
|
||||
impl UsageSnapshot {
|
||||
pub(crate) fn into_query_state(self) -> State {
|
||||
State {
|
||||
identity: self.identity,
|
||||
expanded: self.expanded,
|
||||
contextual_expanded: self.contextual_expanded,
|
||||
..State::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn usage_snapshot(&self) -> UsageSnapshot {
|
||||
UsageSnapshot {
|
||||
identity: self.identity,
|
||||
expanded: self.expanded.clone(),
|
||||
contextual_expanded: self.contextual_expanded.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn owner_exists(p: &Project, owner: Id) -> bool {
|
||||
p.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain))
|
||||
}
|
||||
|
||||
fn occurrence_role(p: &Project, occurrence: Id, subject: Id) -> Option<&'static str> {
|
||||
let e = p.elements.get(&occurrence)?;
|
||||
if occurrence == subject {
|
||||
return Some("contribution");
|
||||
}
|
||||
match e.shared()? {
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Action {
|
||||
action: Some(id), ..
|
||||
},
|
||||
..
|
||||
} if *id == subject => Some("action occurrence"),
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange: Some(id), ..
|
||||
},
|
||||
..
|
||||
} if *id == subject => Some("exchange occurrence"),
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::SequenceReference { sequence: Some(id) },
|
||||
..
|
||||
} if *id == subject => Some("reused process / chain"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Graph<'_> {
|
||||
fn usage_bounds(&self, target: &Target) -> Option<Rectangle> {
|
||||
match target {
|
||||
Target::Whole => Some(Rectangle::new(Point::ORIGIN, self.size)),
|
||||
Target::ProcessOccurrence(endpoint) => {
|
||||
let root = endpoint
|
||||
.references
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(endpoint.occurrence);
|
||||
let node = self.nodes.iter().find(|node| node.id == root)?;
|
||||
if endpoint.references.is_empty() {
|
||||
Some(node.rect)
|
||||
} else {
|
||||
// endpoint_rect intentionally has a proxy fallback for other callers;
|
||||
// a usage promises an actual tile and must never take that fallback.
|
||||
node.preview
|
||||
.iter()
|
||||
.any(|(shown, _)| shown == endpoint)
|
||||
.then(|| node.endpoint_rect(endpoint))
|
||||
}
|
||||
}
|
||||
Target::ProcessLink(id) => {
|
||||
let edge = self.edges.iter().find(|edge| edge.id == *id)?;
|
||||
if !edge.guard.is_empty() {
|
||||
Some(edge.label)
|
||||
} else {
|
||||
let segment = edge
|
||||
.points
|
||||
.windows(2)
|
||||
.nth(edge.points.len().saturating_sub(1) / 2)?;
|
||||
let center = Point::new(
|
||||
(segment[0].x + segment[1].x) / 2.,
|
||||
(segment[0].y + segment[1].y) / 2.,
|
||||
);
|
||||
Some(Rectangle::new(
|
||||
center - iced::Vector::new(16., 16.),
|
||||
Size::new(32., 32.),
|
||||
))
|
||||
}
|
||||
}
|
||||
Target::ProcessPreviewLink {
|
||||
relation,
|
||||
source,
|
||||
target,
|
||||
} => {
|
||||
let node = self
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| source.references.first() == Some(&node.id))?;
|
||||
let link = node.preview_links.iter().find(|link| {
|
||||
link.relation == *relation && link.source == *source && link.target == *target
|
||||
})?;
|
||||
let [a, b] = node.preview_link_visible_segment(link)?;
|
||||
Some(Rectangle::new(
|
||||
Point::new((a.x + b.x) / 2. - 16., (a.y + b.y) / 2. - 16.),
|
||||
Size::new(32., 32.),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Inactive owners use the same default preview policy as entering their canvas.
|
||||
fn usage_graph<'a>(&self, p: &'a Project, owner: Id) -> Graph<'a> {
|
||||
if self.identity.is_some_and(|(_, current)| current == owner) {
|
||||
Graph::expanded_qualified(p, owner, owner, &self.expanded, &self.contextual_expanded)
|
||||
} else {
|
||||
Graph::new(p, owner, owner)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn diagram_usages(&self, p: &Project, owner: Id, subject: Id) -> Vec<Usage> {
|
||||
if !owner_exists(p, owner)
|
||||
|| !(p.elements.contains_key(&subject) || p.relations.contains_key(&subject))
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
let graph = self.usage_graph(p, owner);
|
||||
let mut usages = vec![];
|
||||
let mut add = |target: Target, role: &str| {
|
||||
if let Some(bounds) = graph.usage_bounds(&target) {
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface: Surface::Process(owner),
|
||||
target,
|
||||
role: role.into(),
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
};
|
||||
if subject == owner {
|
||||
add(Target::Whole, "process / chain canvas");
|
||||
}
|
||||
for node in &graph.nodes {
|
||||
if let Some(role) = occurrence_role(p, node.id, subject) {
|
||||
add(
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: node.id,
|
||||
}),
|
||||
role,
|
||||
);
|
||||
}
|
||||
for (endpoint, _) in &node.preview {
|
||||
if let Some(role) = occurrence_role(p, endpoint.occurrence, subject) {
|
||||
add(
|
||||
Target::ProcessOccurrence(endpoint.clone()),
|
||||
&format!("reused {role}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
for link in &node.preview_links {
|
||||
if link.relation == subject {
|
||||
add(
|
||||
Target::ProcessPreviewLink {
|
||||
relation: link.relation,
|
||||
source: link.source.clone(),
|
||||
target: link.target.clone(),
|
||||
},
|
||||
"reused sequence link",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for edge in &graph.edges {
|
||||
let role = if edge.id == subject {
|
||||
Some("process link")
|
||||
} else {
|
||||
match p.relations.get(&edge.id).map(|r| &r.body) {
|
||||
Some(RelationBody::ContextualLink {
|
||||
purpose: ContextualLinkPurpose::Exchange { exchange },
|
||||
..
|
||||
}) if *exchange == subject => Some("assembly exchange"),
|
||||
Some(RelationBody::ContextualLink {
|
||||
purpose: ContextualLinkPurpose::CommonWork { work },
|
||||
..
|
||||
}) if *work == subject => Some("common work"),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if let Some(role) = role {
|
||||
add(Target::ProcessLink(edge.id), role);
|
||||
}
|
||||
}
|
||||
usages
|
||||
}
|
||||
|
||||
/// Returns success, not whether the camera changed. App must separately guard
|
||||
/// its other drafts and revalidate the queried subject's current usage.
|
||||
pub(crate) fn reveal_usage(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
document: Id,
|
||||
owner: Id,
|
||||
target: &Target,
|
||||
) -> bool {
|
||||
if !owner_exists(p, owner) || self.draft.is_some() {
|
||||
return false;
|
||||
}
|
||||
let same = self.identity == Some((document, owner));
|
||||
let graph = if same {
|
||||
self.usage_graph(p, owner)
|
||||
} else {
|
||||
Graph::new(p, owner, owner)
|
||||
};
|
||||
let Some(bounds) = graph.usage_bounds(target) else {
|
||||
return false;
|
||||
};
|
||||
let address = if let Target::ProcessOccurrence(endpoint) = target {
|
||||
let Ok(resolved) =
|
||||
oa_core::topology_context::TopologyIndex::new(p).resolve(owner, endpoint)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
(!endpoint.references.is_empty()).then_some(resolved.address)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let viewport = self.viewport.unwrap_or(Size::new(500., 300.));
|
||||
self.enter(document, owner);
|
||||
if !same {
|
||||
self.contextual_expanded.clear();
|
||||
}
|
||||
self.selected_context = address;
|
||||
self.tool = CanvasTool::Select;
|
||||
self.viewport = Some(viewport);
|
||||
let fit = ((viewport.width - 48.).max(1.) / bounds.width.max(1.))
|
||||
.min((viewport.height - 48.).max(1.) / bounds.height.max(1.));
|
||||
let scale = if matches!(target, Target::Whole) {
|
||||
fit.clamp(0.0001, 1.)
|
||||
} else {
|
||||
fit.clamp(0.6, 1.25)
|
||||
};
|
||||
self.zoom = Some(scale);
|
||||
self.fit = false;
|
||||
self.pan = iced::Vector::new(
|
||||
viewport.width / 2. - bounds.center_x() * scale,
|
||||
viewport.height / 2. - bounds.center_y() * scale,
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,439 @@
|
||||
use super::*;
|
||||
|
||||
fn add(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let e = occurrence(p, owner, name, body);
|
||||
let id = e.id;
|
||||
*p = execute_batch(p, &[Command::CreateElement(e)]).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
fn reused_fixture() -> (Project, Id, Id, Id, [Id; 2], Vec<Id>) {
|
||||
let f = super::super::direct_tests::fixture(Layer::Oa);
|
||||
let mut p = f.p;
|
||||
let sequence = p
|
||||
.make_element(Kind::Process, Layer::Oa, "Reviewed checks", None)
|
||||
.unwrap();
|
||||
let sequence_id = sequence.id;
|
||||
p = execute_batch(&p, &[Command::CreateElement(sequence)]).unwrap();
|
||||
let mut leaves = vec![];
|
||||
for name in [
|
||||
"Check one",
|
||||
"Check two",
|
||||
"Check three",
|
||||
"Check four",
|
||||
"Check five",
|
||||
] {
|
||||
leaves.push(add(
|
||||
&mut p,
|
||||
sequence_id,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work[0]),
|
||||
lifeline: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
let first = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"First invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence_id),
|
||||
},
|
||||
);
|
||||
let second = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Second invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence_id),
|
||||
},
|
||||
);
|
||||
(p, f.owner, sequence_id, f.work[0], [first, second], leaves)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usages_exclude_endpoint_coincidence_and_keep_actual_occurrences_and_links() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = super::super::direct_tests::fixture(layer);
|
||||
let mut p = f.p;
|
||||
let state = State::default();
|
||||
// Both exchange endpoints have Action contributions, but there is no
|
||||
// Exchange occurrence and no exchange arrow in this process.
|
||||
assert!(state.diagram_usages(&p, f.owner, f.exchange).is_empty());
|
||||
let direct = state.diagram_usages(&p, f.owner, f.work[0]);
|
||||
assert_eq!(direct.len(), 1);
|
||||
assert_eq!(
|
||||
direct[0].target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: f.nodes[0]
|
||||
})
|
||||
);
|
||||
let link = state.diagram_usages(&p, f.owner, f.link);
|
||||
assert_eq!(link.len(), 1);
|
||||
assert_eq!(link[0].target, Target::ProcessLink(f.link));
|
||||
let first = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"First exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let second = add(
|
||||
&mut p,
|
||||
f.owner,
|
||||
"Second exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
);
|
||||
let usages = state.diagram_usages(&p, f.owner, f.exchange);
|
||||
assert_eq!(usages.len(), 2);
|
||||
assert_eq!(
|
||||
usages
|
||||
.iter()
|
||||
.map(|usage| usage.target.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: first
|
||||
}),
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: second
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usages_keep_caller_paths_and_exclude_unshown_fifth_preview() {
|
||||
let (p, owner, sequence, work, calls, leaves) = reused_fixture();
|
||||
let before = p.clone();
|
||||
let mut state = State::default();
|
||||
let leaf = state.diagram_usages(&p, owner, leaves[0]);
|
||||
assert_eq!(leaf.len(), 2);
|
||||
for (usage, call) in leaf.iter().zip(calls) {
|
||||
assert_eq!(
|
||||
usage.target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[0]
|
||||
})
|
||||
);
|
||||
}
|
||||
assert_ne!(leaf[0].bounds, leaf[1].bounds);
|
||||
assert!(state.diagram_usages(&p, owner, leaves[4]).is_empty());
|
||||
assert_eq!(state.diagram_usages(&p, owner, sequence).len(), 2);
|
||||
// One local action and four tiles in each of two caller boxes.
|
||||
assert_eq!(state.diagram_usages(&p, owner, work).len(), 9);
|
||||
state.enter(Id::new_v4(), owner);
|
||||
assert!(state.expand(&p, owner, calls[0]));
|
||||
let fifth = state.diagram_usages(&p, owner, leaves[4]);
|
||||
assert_eq!(fifth.len(), 1);
|
||||
assert_eq!(
|
||||
fifth[0].target,
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[4]
|
||||
})
|
||||
);
|
||||
// Another active owner must not leak its expansion to this queried canvas.
|
||||
state.enter(Id::new_v4(), sequence);
|
||||
assert!(state.diagram_usages(&p, owner, leaves[4]).is_empty());
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_usage_reveal_validates_before_state_changes_and_centers_the_exact_tile() {
|
||||
let (p, owner, _, _, calls, leaves) = reused_fixture();
|
||||
let before = p.clone();
|
||||
let document = Id::new_v4();
|
||||
let mut state = State::default();
|
||||
state.enter(document, owner);
|
||||
state.viewport(Size::new(880., 460.));
|
||||
state.pan = iced::Vector::new(-3000., -900.);
|
||||
let hidden = Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[4],
|
||||
});
|
||||
let prior = (
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone(),
|
||||
);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &hidden));
|
||||
assert_eq!(
|
||||
(
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone()
|
||||
),
|
||||
prior
|
||||
);
|
||||
assert!(!state.reveal_usage(&p, document, Id::new_v4(), &Target::Whole));
|
||||
assert_eq!(
|
||||
(
|
||||
state.identity,
|
||||
state.pan,
|
||||
state.zoom,
|
||||
state.fit,
|
||||
state.selected_context.clone()
|
||||
),
|
||||
prior
|
||||
);
|
||||
let endpoint = OccurrenceEndpoint {
|
||||
references: vec![calls[1]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let target = Target::ProcessOccurrence(endpoint.clone());
|
||||
assert!(state.reveal_usage(&p, document, owner, &target));
|
||||
assert_eq!(state.selected_context.as_ref().unwrap().endpoint, endpoint);
|
||||
let bounds = state
|
||||
.diagram_usages(&p, owner, leaves[0])
|
||||
.into_iter()
|
||||
.find(|usage| usage.target == target)
|
||||
.unwrap()
|
||||
.bounds;
|
||||
let scale = state.zoom.unwrap();
|
||||
assert!(scale >= 0.6);
|
||||
assert!((bounds.center_x() * scale + state.pan.x - 440.).abs() < 0.01);
|
||||
assert!((bounds.center_y() * scale + state.pan.y - 230.).abs() < 0.01);
|
||||
assert!(state.reveal_usage(&p, document, owner, &Target::Whole));
|
||||
assert!(state.selected_context.is_none());
|
||||
state
|
||||
.update(&p, Event::Start(owner, Tool::Work, None))
|
||||
.unwrap();
|
||||
let draft = state.recovery_snapshot().unwrap();
|
||||
let camera = (state.pan, state.zoom, state.fit);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &target));
|
||||
assert_eq!(state.recovery_snapshot().unwrap(), draft);
|
||||
assert_eq!((state.pan, state.zoom, state.fit), camera);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_contextual_purpose_and_painted_preview_links_keep_distinct_identities() {
|
||||
let (mut p, owner, sequence, work, calls, leaves) = reused_fixture();
|
||||
let source = OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let target = OccurrenceEndpoint {
|
||||
references: vec![calls[1]],
|
||||
occurrence: leaves[0],
|
||||
};
|
||||
let relation = Relation {
|
||||
id: Id::new_v4(),
|
||||
alias: "same_check".into(),
|
||||
name: "One common check".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::ContextualLink {
|
||||
owner,
|
||||
source,
|
||||
target,
|
||||
purpose: ContextualLinkPurpose::CommonWork { work },
|
||||
},
|
||||
};
|
||||
let contextual = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let Command::CreateRelation(relation) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let local_link = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let state = State::default();
|
||||
assert!(
|
||||
state
|
||||
.diagram_usages(&p, owner, work)
|
||||
.iter()
|
||||
.any(|usage| usage.target == Target::ProcessLink(contextual)
|
||||
&& usage.role == "common work")
|
||||
);
|
||||
assert_eq!(state.diagram_usages(&p, owner, contextual).len(), 1);
|
||||
let previews = state.diagram_usages(&p, owner, local_link);
|
||||
assert_eq!(previews.len(), 2);
|
||||
for (usage, call) in previews.iter().zip(calls) {
|
||||
assert_eq!(
|
||||
usage.target,
|
||||
Target::ProcessPreviewLink {
|
||||
relation: local_link,
|
||||
source: OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[0]
|
||||
},
|
||||
target: OccurrenceEndpoint {
|
||||
references: vec![call],
|
||||
occurrence: leaves[1]
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_eq!(state.diagram_usages(&p, sequence, local_link).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_arrow_focus_is_in_the_painted_gap_and_hidden_endpoints_never_fallback() {
|
||||
let (mut p, owner, _, _, calls, leaves) = reused_fixture();
|
||||
let Command::CreateRelation(shown) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let shown_id = shown.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(shown)]).unwrap();
|
||||
let Command::CreateRelation(hidden) = link(&p, leaves[0], leaves[4], "later") else {
|
||||
unreachable!()
|
||||
};
|
||||
let hidden_id = hidden.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(hidden)]).unwrap();
|
||||
let before = p.clone();
|
||||
let document = Id::new_v4();
|
||||
let mut state = State::default();
|
||||
state.enter(document, owner);
|
||||
state.viewport(Size::new(800., 460.));
|
||||
let rows = state.diagram_usages(&p, owner, shown_id);
|
||||
assert_eq!(rows.len(), 2);
|
||||
let graph = Graph::new(&p, owner, owner);
|
||||
for (row, call) in rows.iter().zip(calls) {
|
||||
let node = graph.nodes.iter().find(|n| n.id == call).unwrap();
|
||||
// The painter places 113px tiles 125px apart, beginning 12px in.
|
||||
// The 12px gap is centered 131px into the caller, 134px below its top.
|
||||
assert_eq!(
|
||||
row.bounds.center(),
|
||||
Point::new(node.rect.x + 131., node.rect.y + 134.)
|
||||
);
|
||||
assert!(
|
||||
node.preview
|
||||
.iter()
|
||||
.all(|(e, _)| !node.endpoint_rect(e).contains(row.bounds.center()))
|
||||
);
|
||||
}
|
||||
assert_ne!(rows[0].bounds, rows[1].bounds);
|
||||
assert!(state.diagram_usages(&p, owner, hidden_id).is_empty());
|
||||
assert!(state.reveal_usage(&p, document, owner, &rows[1].target));
|
||||
assert!(
|
||||
state.selected_context.is_none(),
|
||||
"an arrow is not an occurrence authoring context"
|
||||
);
|
||||
let zoom = state.zoom.unwrap();
|
||||
assert!((rows[1].bounds.center_x() * zoom + state.pan.x - 400.).abs() < 0.01);
|
||||
assert!((rows[1].bounds.center_y() * zoom + state.pan.y - 230.).abs() < 0.01);
|
||||
assert!(state.expand(&p, owner, calls[0]));
|
||||
let expanded = state.diagram_usages(&p, owner, hidden_id);
|
||||
assert_eq!(expanded.len(), 1);
|
||||
assert!(
|
||||
matches!(&expanded[0].target, Target::ProcessPreviewLink { source, .. }
|
||||
if source.references == vec![calls[0]])
|
||||
);
|
||||
let mut stale = rows[1].target.clone();
|
||||
if let Target::ProcessPreviewLink { source, .. } = &mut stale {
|
||||
source.references = vec![Id::new_v4()];
|
||||
}
|
||||
let prior = (state.pan, state.zoom, state.fit);
|
||||
assert!(!state.reveal_usage(&p, document, owner, &stale));
|
||||
assert_eq!((state.pan, state.zoom, state.fit), prior);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visible_preview_segment_excludes_all_opaque_tiles_for_reverse_and_cross_row_arrows() {
|
||||
let tiles = [
|
||||
Rectangle::new(Point::new(12., 118.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(137., 118.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(12., 162.), Size::new(113., 32.)),
|
||||
Rectangle::new(Point::new(137., 162.), Size::new(113., 32.)),
|
||||
];
|
||||
for points in [
|
||||
[Point::new(250., 134.), Point::new(12., 134.)],
|
||||
[Point::new(125., 134.), Point::new(137., 178.)],
|
||||
[Point::new(250., 178.), Point::new(12., 134.)],
|
||||
] {
|
||||
let [a, b] = visible_preview_segment(points, tiles.into_iter()).unwrap();
|
||||
for fraction in [0.1, 0.5, 0.9] {
|
||||
let point = a + (b - a) * fraction;
|
||||
assert!(tiles.iter().all(|tile| !tile.contains(point)));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
visible_preview_segment(
|
||||
[Point::new(30., 134.), Point::new(100., 134.)],
|
||||
tiles.into_iter()
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
visible_preview_segment([Point::new(1., 1.), Point::new(1., 1.)], tiles.into_iter())
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanded_nested_tiles_do_not_invent_links_the_preview_painter_does_not_emit() {
|
||||
let (mut p, owner, sequence, work, calls, _) = reused_fixture();
|
||||
let nested = p
|
||||
.make_element(Kind::Process, Layer::Oa, "Nested checks", None)
|
||||
.unwrap();
|
||||
let nested_id = nested.id;
|
||||
p = execute_batch(&p, &[Command::CreateElement(nested)]).unwrap();
|
||||
let leaves: Vec<_> = ["Nested one", "Nested two"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
add(
|
||||
&mut p,
|
||||
nested_id,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let Command::CreateRelation(relation) = link(&p, leaves[0], leaves[1], "next") else {
|
||||
unreachable!()
|
||||
};
|
||||
let relation_id = relation.id;
|
||||
p = execute_batch(&p, &[Command::CreateRelation(relation)]).unwrap();
|
||||
let reference = add(
|
||||
&mut p,
|
||||
sequence,
|
||||
"Nested invocation",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(nested_id),
|
||||
},
|
||||
);
|
||||
let mut state = State::default();
|
||||
state.enter(Id::new_v4(), owner);
|
||||
state.expanded.insert(calls[0]);
|
||||
state.contextual_expanded.insert(OccurrenceEndpoint {
|
||||
references: vec![calls[0]],
|
||||
occurrence: reference,
|
||||
});
|
||||
let shown = state.diagram_usages(&p, owner, leaves[0]);
|
||||
assert_eq!(shown.len(), 1);
|
||||
assert!(
|
||||
matches!(&shown[0].target, Target::ProcessOccurrence(endpoint)
|
||||
if endpoint.references == vec![calls[0], reference])
|
||||
);
|
||||
assert!(state.diagram_usages(&p, owner, relation_id).is_empty());
|
||||
assert_eq!(state.diagram_usages(&p, nested_id, relation_id).len(), 1);
|
||||
}
|
||||
@@ -183,6 +183,32 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
|
||||
&state.expanded,
|
||||
&state.contextual_expanded,
|
||||
);
|
||||
graph.selected_preview = state
|
||||
.qualified_selection(p, owner)
|
||||
.filter(|address| {
|
||||
address
|
||||
.endpoint
|
||||
.references
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(address.endpoint.occurrence)
|
||||
== app.selected
|
||||
})
|
||||
.map(|address| address.endpoint.clone());
|
||||
graph.selected_preview_link = app
|
||||
.diagram_focus(crate::diagram_usage::Surface::Process(owner))
|
||||
.and_then(|usage| match &usage.target {
|
||||
crate::diagram_usage::Target::ProcessPreviewLink {
|
||||
relation,
|
||||
source,
|
||||
target,
|
||||
} => Some(PreviewLink {
|
||||
relation: *relation,
|
||||
source: source.clone(),
|
||||
target: target.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
|
||||
graph.camera_pan = state.pan;
|
||||
graph.pending_insertion = state
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct DiagramCanvas<'a> {
|
||||
pub view_index: Option<&'a oa_core::view_options::ViewIndex>,
|
||||
pub diagram: Id,
|
||||
pub selected: Id,
|
||||
/// Presentation-only navigation marker; never used by hit tests or edits.
|
||||
pub navigation_focus: Option<Rectangle>,
|
||||
pub zoom: f32,
|
||||
pub pan: Vector,
|
||||
pub context: Context,
|
||||
@@ -1193,6 +1195,14 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
f.fill(&Path::circle(center, 1.5), color);
|
||||
}
|
||||
}
|
||||
if let Some(bounds) = self.navigation_focus {
|
||||
f.stroke(
|
||||
&Path::rectangle(bounds.position(), bounds.size()),
|
||||
Stroke::default()
|
||||
.with_color(style::TEAL)
|
||||
.with_width(2.5 / self.zoom),
|
||||
);
|
||||
}
|
||||
if let Some(edge) = scene.routed.edges.get(&self.selected)
|
||||
&& !edge.custom_suspended
|
||||
{
|
||||
@@ -1902,6 +1912,7 @@ mod tests {
|
||||
connection: None,
|
||||
diagram,
|
||||
selected: p.id,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::ZERO,
|
||||
};
|
||||
@@ -1976,6 +1987,7 @@ mod tests {
|
||||
connection: None,
|
||||
diagram,
|
||||
selected: p.id,
|
||||
navigation_focus: None,
|
||||
zoom: 0.6,
|
||||
pan: Vector::new(10., 20.),
|
||||
};
|
||||
@@ -2092,6 +2104,7 @@ mod tests {
|
||||
diagram,
|
||||
context,
|
||||
selected: p.id,
|
||||
navigation_focus: None,
|
||||
selected_bend: None,
|
||||
zoom,
|
||||
pan: Vector::new(20., 30.),
|
||||
@@ -2259,6 +2272,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: edge,
|
||||
navigation_focus: None,
|
||||
zoom,
|
||||
pan: Vector::new(150., 300.),
|
||||
context: Context {
|
||||
@@ -2334,6 +2348,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: edge,
|
||||
navigation_focus: None,
|
||||
zoom: 0.5,
|
||||
pan: Vector::new(150., 300.),
|
||||
context: Context {
|
||||
@@ -2430,6 +2445,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: edge,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::new(150., 300.),
|
||||
context: Context {
|
||||
@@ -2557,6 +2573,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: edge,
|
||||
navigation_focus: None,
|
||||
zoom: 0.7,
|
||||
pan: Vector::new(350., 350.),
|
||||
context: Context {
|
||||
@@ -2610,6 +2627,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: p.id,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::new(100., 400.),
|
||||
context: Context {
|
||||
@@ -2683,6 +2701,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: edge,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::new(100., 400.),
|
||||
context: Context {
|
||||
@@ -2715,6 +2734,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::ZERO,
|
||||
context: Context {
|
||||
|
||||
@@ -57,6 +57,7 @@ fn canvas(app: &App) -> DiagramCanvas<'_> {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
|
||||
@@ -65,6 +65,7 @@ fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
navigation_focus: None,
|
||||
zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
|
||||
@@ -6,6 +6,7 @@ fn canvas(project: &Project, diagram: Id, zoom: f32) -> DiagramCanvas<'_> {
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: Id::nil(),
|
||||
navigation_focus: None,
|
||||
zoom,
|
||||
pan: Vector::new(17., 23.),
|
||||
context: Context {
|
||||
|
||||
@@ -178,6 +178,7 @@ mod tests {
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
navigation_focus: None,
|
||||
zoom: 1.,
|
||||
pan: Vector::ZERO,
|
||||
context: app.route_context().unwrap(),
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
//! Read-only identities for objects actually represented by a diagram renderer.
|
||||
use iced::Rectangle;
|
||||
use oa_core::{Id, OccurrenceEndpoint, Project, projection, routing, view_options::ViewIndex};
|
||||
pub(crate) mod view;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Context {
|
||||
pub document: Id,
|
||||
pub revision: u64,
|
||||
pub subject: Id,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Event {
|
||||
Open(Context),
|
||||
Close(Id),
|
||||
Search(Id, String),
|
||||
Page(Id, usize),
|
||||
Destination(Id, Option<Surface>),
|
||||
Choose(Id, Usage),
|
||||
Move(Id, i8),
|
||||
Activate(Id),
|
||||
}
|
||||
|
||||
/// One completed immutable accepted-model lookup. The token owns one worker slot.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Completion {
|
||||
pub token: Id,
|
||||
pub context: Context,
|
||||
pub result: Result<Vec<Usage>, String>,
|
||||
}
|
||||
|
||||
pub(crate) struct Panel {
|
||||
pub token: Id,
|
||||
pub context: Context,
|
||||
pub usages: Vec<Usage>,
|
||||
pub loading: bool,
|
||||
pub search: String,
|
||||
pub page: usize,
|
||||
pub cursor: usize,
|
||||
pub destination: Option<Surface>,
|
||||
pub notice: Option<String>,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
pub fn new(context: Context, usages: Vec<Usage>) -> Self {
|
||||
Self {
|
||||
token: Id::new_v4(),
|
||||
context,
|
||||
usages,
|
||||
loading: false,
|
||||
search: String::new(),
|
||||
page: 0,
|
||||
cursor: 0,
|
||||
destination: None,
|
||||
notice: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches<'a>(&'a self, p: &Project) -> Vec<&'a Usage> {
|
||||
let query = self.search.to_lowercase();
|
||||
if query.is_empty() {
|
||||
return self.usages.iter().collect();
|
||||
}
|
||||
self.usages
|
||||
.iter()
|
||||
.filter(|usage| {
|
||||
let surface = usage.surface;
|
||||
let searchable = format!(
|
||||
"{} {} {} {} {}",
|
||||
surface.category(p),
|
||||
p.named(surface.id()),
|
||||
alias(p, surface.id()),
|
||||
target_name(p, usage),
|
||||
usage.role
|
||||
);
|
||||
searchable.to_lowercase().contains(&query)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn choices(&self, p: &Project) -> Vec<Event> {
|
||||
if self.loading {
|
||||
return Vec::new();
|
||||
}
|
||||
let matches = self.matches(p);
|
||||
if let Some(surface) = self.destination {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter(|u| u.surface == surface)
|
||||
.map(|u| Event::Choose(self.token, u.clone()))
|
||||
.collect()
|
||||
} else {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
matches
|
||||
.into_iter()
|
||||
.filter(|u| seen.insert(u.surface))
|
||||
.map(|u| Event::Destination(self.token, Some(u.surface)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum Surface {
|
||||
Saved(Id),
|
||||
Process(Id),
|
||||
Scenario(Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Target {
|
||||
Whole,
|
||||
Placement(Id),
|
||||
Port {
|
||||
port: Id,
|
||||
placement: Id,
|
||||
},
|
||||
Edge(Id),
|
||||
StateRow {
|
||||
placement: Id,
|
||||
relation: Id,
|
||||
},
|
||||
AllocationGrouping {
|
||||
relation: Id,
|
||||
work_placement: Id,
|
||||
performer_placement: Id,
|
||||
},
|
||||
ProcessOccurrence(OccurrenceEndpoint),
|
||||
ProcessLink(Id),
|
||||
ProcessPreviewLink {
|
||||
relation: Id,
|
||||
source: OccurrenceEndpoint,
|
||||
target: OccurrenceEndpoint,
|
||||
},
|
||||
ScenarioObject(Id),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Usage {
|
||||
pub subject: Id,
|
||||
pub surface: Surface,
|
||||
pub target: Target,
|
||||
/// Concise, truthful explanation, e.g. "action occurrence" or "entry action".
|
||||
pub role: String,
|
||||
/// Actual current renderer coordinates. Recompute before activation.
|
||||
pub bounds: Rectangle,
|
||||
}
|
||||
|
||||
/// A navigation highlight is presentation state, never an edit/Delete target.
|
||||
pub(crate) struct Focus {
|
||||
pub context: Context,
|
||||
pub usage: Usage,
|
||||
pub selected: Id,
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
pub fn id(self) -> Id {
|
||||
match self {
|
||||
Self::Saved(id) | Self::Process(id) | Self::Scenario(id) => id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(self, p: &Project) -> &'static str {
|
||||
match self {
|
||||
Self::Saved(id)
|
||||
if p.diagrams
|
||||
.get(&id)
|
||||
.is_some_and(|d| d.projection.state_machine().is_some()) =>
|
||||
{
|
||||
"State views"
|
||||
}
|
||||
Self::Saved(_) => "Architecture",
|
||||
Self::Process(_) => "Processes and chains",
|
||||
Self::Scenario(_) => "Scenarios",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect(
|
||||
p: &Project,
|
||||
subject: Id,
|
||||
process: &crate::behavior::topology::State,
|
||||
scenario: &crate::behavior::scenario::State,
|
||||
) -> Vec<Usage> {
|
||||
let mut usages = saved_usages(p, subject);
|
||||
for element in p.elements.values() {
|
||||
match element.kind() {
|
||||
oa_core::Kind::Process | oa_core::Kind::FunctionalChain => {
|
||||
usages.extend(process.diagram_usages(p, element.id, subject));
|
||||
}
|
||||
oa_core::Kind::Scenario => {
|
||||
usages.extend(scenario.diagram_usages(p, element.id, subject));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
usages.sort_by_cached_key(|u| {
|
||||
(
|
||||
u.surface.category(p),
|
||||
p.named(u.surface.id()).to_lowercase(),
|
||||
u.surface,
|
||||
target_name(p, u).to_lowercase(),
|
||||
)
|
||||
});
|
||||
usages
|
||||
.dedup_by(|a, b| a.subject == b.subject && a.surface == b.surface && a.target == b.target);
|
||||
usages
|
||||
}
|
||||
|
||||
pub(crate) fn alias(p: &Project, id: Id) -> &str {
|
||||
p.elements
|
||||
.get(&id)
|
||||
.map(|e| e.alias.as_str())
|
||||
.or_else(|| p.relations.get(&id).map(|r| r.alias.as_str()))
|
||||
.or_else(|| p.diagrams.get(&id).map(|d| d.alias.as_str()))
|
||||
.or_else(|| p.traces.get(&id).map(|t| t.alias.as_str()))
|
||||
.or_else(|| {
|
||||
p.diagrams
|
||||
.values()
|
||||
.find_map(|d| d.nodes.get(&id).map(|n| n.alias.as_str()))
|
||||
})
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub(crate) fn target_name(p: &Project, usage: &Usage) -> String {
|
||||
let id = match &usage.target {
|
||||
Target::Whole => usage.surface.id(),
|
||||
Target::Placement(id) => p
|
||||
.diagrams
|
||||
.get(&usage.surface.id())
|
||||
.and_then(|d| d.nodes.get(id))
|
||||
.map_or(*id, |n| n.element),
|
||||
Target::Port { port, .. } => *port,
|
||||
Target::Edge(id)
|
||||
| Target::StateRow { relation: id, .. }
|
||||
| Target::AllocationGrouping { relation: id, .. }
|
||||
| Target::ProcessLink(id)
|
||||
| Target::ScenarioObject(id) => *id,
|
||||
Target::ProcessOccurrence(endpoint) => {
|
||||
return endpoint
|
||||
.references
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([endpoint.occurrence])
|
||||
.map(|id| format!("{} (@{})", p.named(id), alias(p, id)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ");
|
||||
}
|
||||
Target::ProcessPreviewLink {
|
||||
relation, source, ..
|
||||
} => {
|
||||
return source
|
||||
.references
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([*relation])
|
||||
.map(|id| format!("{} (@{})", p.named(id), alias(p, id)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ");
|
||||
}
|
||||
};
|
||||
format!("{} (@{})", p.named(id), alias(p, id))
|
||||
}
|
||||
|
||||
/// Saved view membership is the renderer's final projection, including folds,
|
||||
/// projection-kind exclusions and explicit view filters. Stored route metadata
|
||||
/// and merely having both relation endpoints placed do not imply visibility.
|
||||
pub(crate) fn saved_usages(p: &Project, subject: Id) -> Vec<Usage> {
|
||||
saved_usages_for(p, subject, None)
|
||||
}
|
||||
|
||||
/// Revalidate only the requested saved surface before activation. The optional
|
||||
/// filter is applied before projection and routing, never after a whole lookup.
|
||||
pub(crate) fn saved_usages_for(p: &Project, subject: Id, only_diagram: Option<Id>) -> Vec<Usage> {
|
||||
if only_diagram.is_some_and(|id| !p.diagrams.contains_key(&id)) {
|
||||
return Vec::new();
|
||||
}
|
||||
let index = ViewIndex::new(p);
|
||||
let mut usages = Vec::new();
|
||||
for diagram in p
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|diagram| only_diagram.is_none_or(|id| diagram.id == id))
|
||||
{
|
||||
let view = projection::project_diagram_with_index(p, diagram, &index);
|
||||
let surface = Surface::Saved(diagram.id);
|
||||
if subject == diagram.id || diagram.projection.state_machine() == Some(subject) {
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface,
|
||||
target: Target::Whole,
|
||||
role: if subject == diagram.id {
|
||||
"diagram"
|
||||
} else {
|
||||
"state machine view"
|
||||
}
|
||||
.into(),
|
||||
bounds: routed_bounds(routing::route_diagram(p, diagram, &view).bounds),
|
||||
});
|
||||
}
|
||||
for node in &view.nodes {
|
||||
if subject == node.element || subject == node.id {
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface,
|
||||
target: Target::Placement(node.id),
|
||||
role: "node".into(),
|
||||
bounds: Rectangle {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
// Reveal the labelled header, not the empty interior of
|
||||
// a large expanded container at the minimum zoom.
|
||||
width: node.size.width.min(420.),
|
||||
height: node.size.height.min(64.),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for port in &view.ports {
|
||||
if subject == port.element {
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface,
|
||||
target: Target::Port {
|
||||
port: port.element,
|
||||
placement: port.owner,
|
||||
},
|
||||
role: "port".into(),
|
||||
bounds: Rectangle {
|
||||
x: port.position.x - 12.,
|
||||
y: port.position.y - 12.,
|
||||
width: 24.,
|
||||
height: 24.,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Allocation arrows are suppressed in this projection. A direct
|
||||
// relation is represented only by its actual visible containment chain;
|
||||
// aggregate summaries never manufacture inherited relations.
|
||||
if diagram.projection == oa_core::ProjectionKind::Allocation
|
||||
&& let Some(relation) = p.relations.get(&subject)
|
||||
&& relation.kind() == oa_core::RelationKind::Allocation
|
||||
{
|
||||
let placements: std::collections::BTreeMap<_, _> =
|
||||
view.nodes.iter().map(|node| (node.id, node)).collect();
|
||||
for work in view.nodes.iter().filter(|n| n.element == relation.target()) {
|
||||
let mut parent = work.visual_parent;
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
while let Some(id) = parent.filter(|id| seen.insert(*id)) {
|
||||
let Some(performer) = placements.get(&id) else {
|
||||
break;
|
||||
};
|
||||
if performer.element == relation.source() {
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface,
|
||||
target: Target::AllocationGrouping {
|
||||
relation: subject,
|
||||
work_placement: work.id,
|
||||
performer_placement: performer.id,
|
||||
},
|
||||
role: "allocation grouping".into(),
|
||||
bounds: Rectangle {
|
||||
x: work.position.x,
|
||||
y: work.position.y,
|
||||
width: work.size.width.min(420.),
|
||||
height: work.size.height.min(64.),
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
parent = performer.visual_parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if view.edges.iter().any(|edge| edge.id == subject)
|
||||
&& let Some(edge) = routing::route_diagram(p, diagram, &view)
|
||||
.edges
|
||||
.get(&subject)
|
||||
{
|
||||
usages.push(Usage {
|
||||
subject,
|
||||
surface,
|
||||
target: Target::Edge(subject),
|
||||
role: "relationship".into(),
|
||||
bounds: edge_focus_bounds(edge),
|
||||
});
|
||||
}
|
||||
if diagram.projection.state_machine().is_some() {
|
||||
usages.extend(crate::behavior::state_canvas::diagram_usages(
|
||||
p, diagram, &view, subject,
|
||||
));
|
||||
}
|
||||
}
|
||||
usages
|
||||
}
|
||||
|
||||
pub(crate) fn routed_bounds(bounds: routing::RoutingBounds) -> Rectangle {
|
||||
Rectangle {
|
||||
x: bounds.position.x,
|
||||
y: bounds.position.y,
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus a named edge at its label. For an unlabelled line, focus its middle
|
||||
/// segment instead of shrinking a long connector to fit the whole diagram.
|
||||
pub(crate) fn edge_focus_bounds(edge: &routing::RoutedEdge) -> Rectangle {
|
||||
if !edge.label.text.is_empty() {
|
||||
return routed_bounds(edge.label.bounds);
|
||||
}
|
||||
let point = edge
|
||||
.points
|
||||
.get(edge.points.len() / 2)
|
||||
.copied()
|
||||
.unwrap_or(edge.label.center);
|
||||
Rectangle {
|
||||
x: point.x - 40.,
|
||||
y: point.y - 24.,
|
||||
width: 80.,
|
||||
height: 48.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whole-canvas navigation fits its entire extent; an individual target keeps
|
||||
/// the readable minimum zoom of `camera` rather than shrinking unrelated space.
|
||||
pub(crate) fn camera_for(
|
||||
target: &Target,
|
||||
bounds: Rectangle,
|
||||
viewport: iced::Size,
|
||||
) -> (f32, iced::Vector) {
|
||||
if !matches!(target, Target::Whole) {
|
||||
return camera(bounds, viewport);
|
||||
}
|
||||
let available = iced::Size::new(
|
||||
(viewport.width - 64.).max(1.),
|
||||
(viewport.height - 64.).max(1.),
|
||||
);
|
||||
let zoom = (available.width / bounds.width.max(1.))
|
||||
.min(available.height / bounds.height.max(1.))
|
||||
.clamp(0.0001, 1.25);
|
||||
let pan = iced::Vector::new(
|
||||
viewport.width / 2. - bounds.center_x() * zoom,
|
||||
viewport.height / 2. - bounds.center_y() * zoom,
|
||||
);
|
||||
(zoom, pan)
|
||||
}
|
||||
|
||||
/// Return a readable, centered camera without modifying authored geometry.
|
||||
pub(crate) fn camera(bounds: Rectangle, viewport: iced::Size) -> (f32, iced::Vector) {
|
||||
let available = iced::Size::new(
|
||||
(viewport.width - 64.).max(1.),
|
||||
(viewport.height - 64.).max(1.),
|
||||
);
|
||||
let zoom = (available.width / bounds.width.max(1.))
|
||||
.min(available.height / bounds.height.max(1.))
|
||||
.clamp(0.5, 1.25);
|
||||
let pan = iced::Vector::new(
|
||||
viewport.width / 2. - bounds.center_x() * zoom,
|
||||
viewport.height / 2. - bounds.center_y() * zoom,
|
||||
);
|
||||
(zoom, pan)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod coverage_tests;
|
||||
@@ -0,0 +1,386 @@
|
||||
//! Independent allocation membership oracles. Expected targets and coordinates
|
||||
//! come from handwritten fixtures, never from the usage predicate under test.
|
||||
use super::{Surface, Target, Usage, saved_usages, saved_usages_for};
|
||||
use iced::Rectangle;
|
||||
use oa_core::{
|
||||
Id, Kind, Layer, Position, Project, ProjectionKind, RelationKind, Size, projection, validation,
|
||||
};
|
||||
|
||||
struct AllocationFixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
performer: Id,
|
||||
work: Id,
|
||||
relation: Id,
|
||||
performer_node: Id,
|
||||
work_node: Id,
|
||||
}
|
||||
|
||||
fn assert_valid(project: &Project) {
|
||||
let errors: Vec<_> = validation::validate(project)
|
||||
.into_iter()
|
||||
.filter(|issue| issue.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"fixture must be structurally valid: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn fixture() -> AllocationFixture {
|
||||
let mut project = Project::empty();
|
||||
let performer = project
|
||||
.add_element(Kind::Entity, Layer::Oa, "Rescue team", None)
|
||||
.unwrap();
|
||||
let work = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Dispatch", None)
|
||||
.unwrap();
|
||||
let relation = project.relate(RelationKind::Allocation, "Performs", performer, work);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let performer_node = project.place(diagram, performer, Position { x: 20., y: 40. });
|
||||
let work_node = project.place(diagram, work, Position { x: 240., y: 320. });
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&work_node)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 320.,
|
||||
height: 110.,
|
||||
};
|
||||
assert_valid(&project);
|
||||
AllocationFixture {
|
||||
project,
|
||||
diagram,
|
||||
performer,
|
||||
work,
|
||||
relation,
|
||||
performer_node,
|
||||
work_node,
|
||||
}
|
||||
}
|
||||
|
||||
fn expected(f: &AllocationFixture) -> Usage {
|
||||
Usage {
|
||||
subject: f.relation,
|
||||
surface: Surface::Saved(f.diagram),
|
||||
target: Target::AllocationGrouping {
|
||||
relation: f.relation,
|
||||
work_placement: f.work_node,
|
||||
performer_placement: f.performer_node,
|
||||
},
|
||||
role: "allocation grouping".into(),
|
||||
bounds: Rectangle {
|
||||
x: 240.,
|
||||
y: 320.,
|
||||
width: 320.,
|
||||
height: 64.,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Two semantic decomposition levels, both summaries of the one allocated leaf.
|
||||
/// No Allocation relation targets either summary.
|
||||
fn nest_under_summaries(f: &mut AllocationFixture) -> (Id, Id, Id, Id) {
|
||||
let top = f
|
||||
.project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
|
||||
.unwrap();
|
||||
let middle = f
|
||||
.project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Coordinate", Some(top))
|
||||
.unwrap();
|
||||
f.project.elements.get_mut(&f.work).unwrap().parent = Some(middle);
|
||||
let top_node = f
|
||||
.project
|
||||
.place(f.diagram, top, Position { x: 70., y: 120. });
|
||||
let middle_node = f
|
||||
.project
|
||||
.place(f.diagram, middle, Position { x: 130., y: 210. });
|
||||
assert_valid(&f.project);
|
||||
(top, middle, top_node, middle_node)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocation_grouping_has_exact_relation_placements_and_readable_work_geometry() {
|
||||
let f = fixture();
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
let work = view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == f.work_node)
|
||||
.unwrap();
|
||||
assert_eq!(work.visual_parent, Some(f.performer_node));
|
||||
assert_eq!(work.position, Position { x: 240., y: 320. });
|
||||
assert_eq!(
|
||||
work.size,
|
||||
Size {
|
||||
width: 320.,
|
||||
height: 110.
|
||||
}
|
||||
);
|
||||
assert!(!view.edges.iter().any(|edge| edge.id == f.relation));
|
||||
|
||||
assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]);
|
||||
assert_eq!(
|
||||
saved_usages_for(&f.project, f.relation, Some(f.diagram)),
|
||||
vec![expected(&f)]
|
||||
);
|
||||
assert!(saved_usages_for(&f.project, f.relation, Some(Id::new_v4())).is_empty());
|
||||
// The semantic work and its placement remain ordinary node selections.
|
||||
for subject in [f.work, f.work_node] {
|
||||
let usages = saved_usages(&f.project, subject);
|
||||
assert_eq!(usages.len(), 1);
|
||||
assert_eq!(usages[0].target, Target::Placement(f.work_node));
|
||||
}
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocation_grouping_walks_two_visible_summary_levels_without_inventing_allocations() {
|
||||
let mut f = fixture();
|
||||
let (top, middle, top_node, middle_node) = nest_under_summaries(&mut f);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
for (node_id, parent, summary) in [
|
||||
(f.work_node, Some(middle_node), false),
|
||||
(middle_node, Some(top_node), true),
|
||||
(top_node, Some(f.performer_node), true),
|
||||
] {
|
||||
let node = view.nodes.iter().find(|node| node.id == node_id).unwrap();
|
||||
assert_eq!(node.visual_parent, parent);
|
||||
assert_eq!(node.allocation_summary, summary);
|
||||
}
|
||||
assert_eq!(f.project.relations.len(), 1);
|
||||
assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]);
|
||||
for (element, node) in [(top, top_node), (middle, middle_node)] {
|
||||
let usages = saved_usages(&f.project, element);
|
||||
assert_eq!(usages.len(), 1);
|
||||
assert_eq!(usages[0].target, Target::Placement(node));
|
||||
}
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folded_summary_or_performer_does_not_substitute_a_visible_ancestor_for_the_leaf() {
|
||||
let mut f = fixture();
|
||||
let (_, _, top_node, middle_node) = nest_under_summaries(&mut f);
|
||||
for folded in [f.performer_node, top_node, middle_node] {
|
||||
let mut project = f.project.clone();
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&folded)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert_valid(&project);
|
||||
let before = project.clone();
|
||||
let view = projection::project_view(&project, f.diagram);
|
||||
assert!(view.nodes.iter().any(|node| node.id == folded));
|
||||
assert!(!view.nodes.iter().any(|node| node.id == f.work_node));
|
||||
assert!(saved_usages(&project, f.relation).is_empty());
|
||||
assert_eq!(project, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_endpoint_placements_and_hidden_performer_do_not_create_grouping_targets() {
|
||||
let mut f = fixture();
|
||||
let (_, _, top_node, _) = nest_under_summaries(&mut f);
|
||||
for absent in [f.performer_node, f.work_node] {
|
||||
let mut project = f.project.clone();
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&absent);
|
||||
assert_valid(&project);
|
||||
let before = project.clone();
|
||||
let view = projection::project_view(&project, f.diagram);
|
||||
// The aggregate card remains visible even when the actual leaf or its
|
||||
// actual performer has no placement. It cannot stand in for that end.
|
||||
assert!(
|
||||
view.nodes
|
||||
.iter()
|
||||
.any(|node| node.id == top_node && node.allocation_summary)
|
||||
);
|
||||
assert!(saved_usages(&project, f.relation).is_empty());
|
||||
assert_eq!(project, before);
|
||||
}
|
||||
|
||||
let outer = f
|
||||
.project
|
||||
.add_element(Kind::Entity, Layer::Oa, "Organization", None)
|
||||
.unwrap();
|
||||
f.project.elements.get_mut(&f.performer).unwrap().parent = Some(outer);
|
||||
let outer_node = f
|
||||
.project
|
||||
.place(f.diagram, outer, Position { x: -40., y: -60. });
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&outer_node)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
assert!(view.nodes.iter().any(|node| node.id == outer_node));
|
||||
assert!(!view.nodes.iter().any(|node| node.id == f.performer_node));
|
||||
assert!(saved_usages(&f.project, f.relation).is_empty());
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_leaf_and_edge_filter_still_leave_the_actual_nesting_visible() {
|
||||
let mut f = fixture();
|
||||
let diagram = f.project.diagrams.get_mut(&f.diagram).unwrap();
|
||||
diagram.nodes.get_mut(&f.work_node).unwrap().collapsed = true;
|
||||
// hidden_edges controls arrows. Allocation is drawn as nesting, which is
|
||||
// still visible; do not mistake the filter for a hidden node/grouping.
|
||||
diagram.view_options.hidden_edges.push(f.relation);
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
let work = view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == f.work_node)
|
||||
.unwrap();
|
||||
assert!(work.collapsed);
|
||||
assert_eq!(work.visual_parent, Some(f.performer_node));
|
||||
assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]);
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_projections_do_not_report_allocation_nesting_when_only_an_arrow_or_nothing_is_drawn() {
|
||||
let mut f = fixture();
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
assert!(view.edges.iter().any(|edge| edge.id == f.relation));
|
||||
assert!(view.nodes.iter().all(|node| node.visual_parent.is_none()));
|
||||
let usages = saved_usages(&f.project, f.relation);
|
||||
assert_eq!(usages.len(), 1);
|
||||
assert_eq!(usages[0].subject, f.relation);
|
||||
assert_eq!(usages[0].surface, Surface::Saved(f.diagram));
|
||||
assert_eq!(usages[0].target, Target::Edge(f.relation));
|
||||
assert_eq!(f.project, before);
|
||||
|
||||
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::FunctionalDataflow;
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
assert!(view.nodes.iter().any(|node| node.id == f.work_node));
|
||||
assert!(!view.nodes.iter().any(|node| node.id == f.performer_node));
|
||||
assert!(saved_usages(&f.project, f.relation).is_empty());
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_saved_views_keep_allocation_placement_identity_and_geometry_independent() {
|
||||
let mut f = fixture();
|
||||
// The first diagram is Allocation, so this creates a different Overview.
|
||||
let second = f.project.ensure_diagram(Layer::Oa);
|
||||
assert_ne!(second, f.diagram);
|
||||
f.project.diagrams.get_mut(&second).unwrap().projection = ProjectionKind::Allocation;
|
||||
let second_performer = f
|
||||
.project
|
||||
.place(second, f.performer, Position { x: 900., y: 600. });
|
||||
let second_work = f
|
||||
.project
|
||||
.place(second, f.work, Position { x: 1000., y: 700. });
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&second)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&second_work)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 520.,
|
||||
height: 130.,
|
||||
};
|
||||
assert_ne!(second_performer, f.performer_node);
|
||||
assert_ne!(second_work, f.work_node);
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let other = Usage {
|
||||
subject: f.relation,
|
||||
surface: Surface::Saved(second),
|
||||
target: Target::AllocationGrouping {
|
||||
relation: f.relation,
|
||||
work_placement: second_work,
|
||||
performer_placement: second_performer,
|
||||
},
|
||||
role: "allocation grouping".into(),
|
||||
bounds: Rectangle {
|
||||
x: 1000.,
|
||||
y: 700.,
|
||||
width: 420.,
|
||||
height: 64.,
|
||||
},
|
||||
};
|
||||
let usages = saved_usages(&f.project, f.relation);
|
||||
assert_eq!(usages.len(), 2);
|
||||
assert!(usages.contains(&expected(&f)));
|
||||
assert!(usages.contains(&other));
|
||||
assert_eq!(
|
||||
saved_usages_for(&f.project, f.relation, Some(second)),
|
||||
vec![other]
|
||||
);
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_derived_nesting_without_a_direct_allocation_does_not_invent_one() {
|
||||
let mut f = fixture();
|
||||
f.project.relations.remove(&f.relation);
|
||||
let role = f
|
||||
.project
|
||||
.add_element(Kind::Role, Layer::Oa, "Dispatcher", None)
|
||||
.unwrap();
|
||||
let realization = f.project.relate(
|
||||
RelationKind::RoleRealization,
|
||||
"Realizes dispatcher",
|
||||
f.performer,
|
||||
role,
|
||||
);
|
||||
let assignment = f
|
||||
.project
|
||||
.relate(RelationKind::RoleAssignment, "Dispatches", role, f.work);
|
||||
assert_valid(&f.project);
|
||||
let before = f.project.clone();
|
||||
let view = projection::project_view(&f.project, f.diagram);
|
||||
let work = view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == f.work_node)
|
||||
.unwrap();
|
||||
assert_eq!(work.visual_parent, Some(f.performer_node));
|
||||
assert!(work.allocation_summary);
|
||||
assert!(
|
||||
f.project
|
||||
.relations
|
||||
.values()
|
||||
.all(|relation| relation.kind() != RelationKind::Allocation)
|
||||
);
|
||||
for subject in [f.relation, realization, assignment] {
|
||||
assert!(saved_usages(&f.project, subject).is_empty());
|
||||
}
|
||||
let usages = saved_usages(&f.project, f.work);
|
||||
assert_eq!(usages.len(), 1);
|
||||
assert_eq!(usages[0].target, Target::Placement(f.work_node));
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
use super::*;
|
||||
use oa_core::{Kind, Layer, Position, RelationKind};
|
||||
|
||||
#[test]
|
||||
fn saved_membership_respects_filters_and_does_not_change_project() {
|
||||
let mut p = Project::empty();
|
||||
let first = p.add(Kind::Activity, "Same name", 20., 20.);
|
||||
let second = p.add(Kind::Activity, "Same name", 340., 20.);
|
||||
let relation = p.relate(RelationKind::Interaction, "Handoff", first, second);
|
||||
let shown = p.ensure_diagram(Layer::Oa);
|
||||
let mut hidden = p.diagrams[&shown].clone();
|
||||
hidden.id = Id::new_v4();
|
||||
hidden.alias = "filtered_copy".into();
|
||||
hidden.nodes = hidden
|
||||
.nodes
|
||||
.into_values()
|
||||
.map(|mut node| {
|
||||
node.id = Id::new_v4();
|
||||
node.alias = format!("filtered_{}", node.alias);
|
||||
(node.id, node)
|
||||
})
|
||||
.collect();
|
||||
hidden.view_options.hidden_edges.push(relation);
|
||||
let hidden_id = hidden.id;
|
||||
p.diagrams.insert(hidden_id, hidden);
|
||||
let before = p.clone();
|
||||
|
||||
let hits = saved_usages(&p, relation);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].surface, Surface::Saved(shown));
|
||||
assert_eq!(hits[0].target, Target::Edge(relation));
|
||||
let hits = saved_usages(&p, first);
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert!(hits.iter().all(|hit| match hit.target {
|
||||
Target::Placement(node) => p.diagrams[&hit.surface.id()].nodes[&node].element == first,
|
||||
_ => false,
|
||||
}));
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folded_child_is_not_a_visible_node_and_placement_identity_stays_exact() {
|
||||
let mut p = Project::empty();
|
||||
let parent = p.add(Kind::Activity, "Container", 20., 20.);
|
||||
let child = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Nested", Some(parent))
|
||||
.unwrap();
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
let child_node = p.place(diagram, child, Position { x: 50., y: 100. });
|
||||
let parent_node = p.diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == parent)
|
||||
.unwrap()
|
||||
.id;
|
||||
let hits = saved_usages(&p, child_node);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].target, Target::Placement(child_node));
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&parent_node)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let before = p.clone();
|
||||
assert!(saved_usages(&p, child).is_empty());
|
||||
assert!(saved_usages(&p, child_node).is_empty());
|
||||
assert_eq!(saved_usages(&p, parent).len(), 1);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ports_are_found_at_the_visible_owner_and_unplaced_elements_do_not_match_names() {
|
||||
let mut p = Project::empty();
|
||||
let owner = p.add(Kind::Function, "Compute", 20., 20.);
|
||||
let port = Id::new_v4();
|
||||
let element = oa_core::ports::make_port(
|
||||
&p,
|
||||
port,
|
||||
owner,
|
||||
"Input",
|
||||
oa_core::PortContract::FunctionInput {
|
||||
incoming_items: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p = oa_core::execute_batch(&p, &[oa_core::Command::CreateElement(element)]).unwrap();
|
||||
let unrelated = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Compute", None)
|
||||
.unwrap();
|
||||
let hits = saved_usages(&p, port);
|
||||
assert_eq!(hits.len(), 1);
|
||||
let view = projection::project_view(&p, hits[0].surface.id());
|
||||
let painted = view
|
||||
.ports
|
||||
.iter()
|
||||
.find(|shown| shown.element == port)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hits[0].target,
|
||||
Target::Port {
|
||||
port,
|
||||
placement: painted.owner
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
hits[0].bounds.center(),
|
||||
iced::Point::new(painted.position.x, painted.position.y)
|
||||
);
|
||||
assert!(saved_usages(&p, unrelated).is_empty());
|
||||
assert!(saved_usages(&p, Id::new_v4()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_camera_centers_target_without_whole_view_shrink() {
|
||||
let bounds = Rectangle {
|
||||
x: 100_000.,
|
||||
y: -50_000.,
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
let viewport = iced::Size::new(700., 500.);
|
||||
let (zoom, pan) = camera(bounds, viewport);
|
||||
assert_eq!(zoom, 1.25);
|
||||
assert!((bounds.center_x() * zoom + pan.x - 350.).abs() < 0.01);
|
||||
assert!((bounds.center_y() * zoom + pan.y - 250.).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_container_reveal_targets_its_labelled_header() {
|
||||
let mut p = Project::empty();
|
||||
let container = p.add(Kind::Activity, "Large enclosing activity", 40., 70.);
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
let node = p
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap();
|
||||
node.size = oa_core::Size {
|
||||
width: 4000.,
|
||||
height: 4000.,
|
||||
};
|
||||
let before = p.clone();
|
||||
let hits = saved_usages(&p, container);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].bounds.x, 40.);
|
||||
assert_eq!(hits[0].bounds.y, 70.);
|
||||
assert!(hits[0].bounds.width <= 420.);
|
||||
assert!(hits[0].bounds.height <= 64.);
|
||||
let (zoom, pan) = camera(hits[0].bounds, iced::Size::new(500., 300.));
|
||||
assert!((hits[0].bounds.center_x() * zoom + pan.x - 250.).abs() < 0.01);
|
||||
assert!(70. * zoom + pan.y >= 0.);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_machine_owner_finds_its_view_without_a_machine_placement() {
|
||||
let mut p = Project::empty();
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Monitor", None)
|
||||
.unwrap();
|
||||
let unrelated = p
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Other", None)
|
||||
.unwrap();
|
||||
let diagram = p.ensure_diagram(Layer::Sa);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection =
|
||||
oa_core::ProjectionKind::StateMachine { machine };
|
||||
let before = p.clone();
|
||||
let hits = saved_usages(&p, machine);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].surface, Surface::Saved(diagram));
|
||||
assert_eq!(hits[0].target, Target::Whole);
|
||||
assert_eq!(hits[0].role, "state machine view");
|
||||
assert!(saved_usages(&p, unrelated).is_empty());
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_diagram_camera_fits_large_bounds_instead_of_individual_object_scale() {
|
||||
let bounds = Rectangle {
|
||||
x: 2000.,
|
||||
y: -4000.,
|
||||
width: 100_000.,
|
||||
height: 80_000.,
|
||||
};
|
||||
let (zoom, pan) = camera_for(&Target::Whole, bounds, iced::Size::new(500., 300.));
|
||||
assert!(zoom < 0.01);
|
||||
assert!(bounds.x * zoom + pan.x >= 0.);
|
||||
assert!(bounds.y * zoom + pan.y >= 0.);
|
||||
assert!((bounds.x + bounds.width) * zoom + pan.x <= 500.);
|
||||
assert!((bounds.y + bounds.height) * zoom + pan.y <= 300.);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
app::{App, Message},
|
||||
style,
|
||||
views::{action, short},
|
||||
};
|
||||
use iced::{
|
||||
Element, Fill,
|
||||
widget::{self, column, container, row, text, text_input, tooltip},
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
const PAGE_SIZE: usize = 6;
|
||||
|
||||
pub(crate) fn overlay<'a>(app: &'a App, base: Element<'a, Message>) -> Element<'a, Message> {
|
||||
let Some(panel) = &app.diagram_usages else {
|
||||
return base;
|
||||
};
|
||||
let p = app.project();
|
||||
let token = panel.token;
|
||||
let message = |event| Message::DiagramUsage(event);
|
||||
let matched = panel.matches(p);
|
||||
let mut surfaces = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for usage in &matched {
|
||||
if seen.insert(usage.surface) {
|
||||
surfaces.push(usage.surface);
|
||||
}
|
||||
}
|
||||
let targets: Vec<_> = matched
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|usage| Some(usage.surface) == panel.destination)
|
||||
.collect();
|
||||
let count = if panel.destination.is_some() {
|
||||
targets.len()
|
||||
} else {
|
||||
surfaces.len()
|
||||
};
|
||||
let last = count.saturating_sub(1) / PAGE_SIZE;
|
||||
let page = panel.page.min(last);
|
||||
let total_surfaces = panel
|
||||
.usages
|
||||
.iter()
|
||||
.map(|u| u.surface)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len();
|
||||
let mut content = column![
|
||||
row![
|
||||
text("Show in diagrams").size(22).width(Fill),
|
||||
action("Close", message(Event::Close(token)))
|
||||
]
|
||||
.spacing(12),
|
||||
tooltip(
|
||||
text(format!(
|
||||
"{} · @{}",
|
||||
short(p.named(panel.context.subject), 48),
|
||||
short(alias(p, panel.context.subject), 48)
|
||||
))
|
||||
.size(14),
|
||||
container(
|
||||
widget::scrollable(
|
||||
text(format!(
|
||||
"{}\n@{}",
|
||||
p.named(panel.context.subject),
|
||||
alias(p, panel.context.subject)
|
||||
))
|
||||
.size(13)
|
||||
)
|
||||
.height(220)
|
||||
)
|
||||
.padding(10)
|
||||
.width(620)
|
||||
.style(style::card),
|
||||
tooltip::Position::Bottom
|
||||
),
|
||||
text(if panel.loading {
|
||||
"Searching rendered objects…".into()
|
||||
} else {
|
||||
format!(
|
||||
"{} diagram destinations · {} representations",
|
||||
total_surfaces,
|
||||
panel.usages.len()
|
||||
)
|
||||
})
|
||||
.size(12)
|
||||
.color(style::MUTED),
|
||||
text_input("Filter diagram, occurrence, or alias", &panel.search)
|
||||
.id(format!("diagram-usage-filter-{}", panel.token))
|
||||
.padding(9)
|
||||
.on_input(move |value| Message::DiagramUsage(Event::Search(token, value)))
|
||||
.on_submit(message(Event::Activate(token))),
|
||||
]
|
||||
.spacing(10);
|
||||
if let Some(notice) = &panel.notice {
|
||||
content = content.push(text(notice).size(13));
|
||||
}
|
||||
if let Some(surface) = panel.destination {
|
||||
content = content.push(
|
||||
row![
|
||||
action("All diagrams", message(Event::Destination(token, None))),
|
||||
text(short(p.named(surface.id()), 55)).size(15),
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
for (index, usage) in targets
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.skip(page * PAGE_SIZE)
|
||||
.take(PAGE_SIZE)
|
||||
{
|
||||
let name = target_name(p, usage);
|
||||
content = content.push(tooltip(
|
||||
action(
|
||||
&format!("{} · {}", usage.role, short(&name, 70)),
|
||||
message(Event::Choose(token, usage.clone())),
|
||||
)
|
||||
.width(Fill)
|
||||
.style(if panel.cursor == index {
|
||||
style::selected
|
||||
} else {
|
||||
style::nav
|
||||
}),
|
||||
container(text(name).size(13))
|
||||
.padding(10)
|
||||
.max_width(680)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
for (index, surface) in surfaces
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.skip(page * PAGE_SIZE)
|
||||
.take(PAGE_SIZE)
|
||||
{
|
||||
let targets = matched.iter().filter(|u| u.surface == surface).count();
|
||||
let name = format!(
|
||||
"{} · {} (@{}) · {} representations",
|
||||
surface.category(p),
|
||||
p.named(surface.id()),
|
||||
alias(p, surface.id()),
|
||||
targets
|
||||
);
|
||||
let layer = p
|
||||
.diagrams
|
||||
.get(&surface.id())
|
||||
.map(|d| d.layer)
|
||||
.or_else(|| p.layer_of(surface.id()))
|
||||
.map(|l| l.to_string())
|
||||
.unwrap_or_default();
|
||||
content = content.push(tooltip(
|
||||
action(
|
||||
&format!(
|
||||
"{} · {} · {} · {}",
|
||||
surface.category(p),
|
||||
layer,
|
||||
short(p.named(surface.id()), 34),
|
||||
targets
|
||||
),
|
||||
message(Event::Destination(token, Some(surface))),
|
||||
)
|
||||
.width(Fill)
|
||||
.style(if panel.cursor == index {
|
||||
style::selected
|
||||
} else {
|
||||
style::nav
|
||||
}),
|
||||
container(text(name).size(13))
|
||||
.padding(10)
|
||||
.max_width(680)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
));
|
||||
}
|
||||
}
|
||||
if panel.loading {
|
||||
content = content.push(text("Finding diagram representations…").size(13));
|
||||
} else if count == 0 {
|
||||
content = content.push(text(if panel.usages.is_empty() {
|
||||
"No currently rendered representation found. Folded or filtered objects and unexpanded reuse contents are excluded."
|
||||
} else { "No results match this filter." }).size(13));
|
||||
}
|
||||
let mut paging = row![
|
||||
text(format!(
|
||||
"Page {} of {} · {} matches",
|
||||
page + 1,
|
||||
last + 1,
|
||||
count
|
||||
))
|
||||
.size(12)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(8);
|
||||
if page > 0 {
|
||||
paging = paging.push(action("Previous", message(Event::Page(token, page - 1))));
|
||||
}
|
||||
if page < last {
|
||||
paging = paging.push(action("Next", message(Event::Page(token, page + 1))));
|
||||
}
|
||||
content = content.push(paging).push(text("↑ ↓ choose · Enter opens · Escape closes. Source and saved view settings are preserved.").size(12).color(style::MUTED));
|
||||
widget::stack![
|
||||
base,
|
||||
widget::opaque(
|
||||
container(
|
||||
container(content)
|
||||
.padding(22)
|
||||
.width((app.window_size.width - 80.).clamp(620., 820.))
|
||||
.style(style::card)
|
||||
)
|
||||
.center(Fill)
|
||||
.style(|_| container::Style {
|
||||
background: Some(iced::Color::from_rgba(0.08, 0.12, 0.18, 0.38).into()),
|
||||
..Default::default()
|
||||
})
|
||||
)
|
||||
]
|
||||
.into()
|
||||
}
|
||||
@@ -805,7 +805,7 @@ fn article(topic: Topic) -> Article {
|
||||
const {
|
||||
Article {
|
||||
summary: "The text editor and graphical forms edit the same model. Source projects preserve file organization and comments for human review, automation and version control.",
|
||||
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename diff merge restructuredtext spec canonical parity",
|
||||
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename diff merge restructuredtext spec canonical parity definition references lexical navigation F12 back",
|
||||
sections: &[
|
||||
walkthrough(
|
||||
"Edit and review source",
|
||||
@@ -818,6 +818,18 @@ fn article(topic: Topic) -> Article {
|
||||
"Save all writes the entry and its included UTF-8 files. Review the Git diff before committing, including relationships, traces and diagram changes that belong to the same engineering edit.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
"Follow definitions and lexical references",
|
||||
"Place the caret on a declaration alias or an @reference. Choose Definition (F12) to locate its declaration, or References (Shift+F12) to list lexical uses. Choose a row when a definition is ambiguous, draft or detached. Lexical uses exclude comments and strings; they do not establish typed relationships or resolve an ambiguous alias to one declaration. Back (Alt+Left) returns to the previous source location; if that draft changed, it restores the current cursor instead of an obsolete selection. These shortcuts work while the source editor has focus; the Text toolbar also offers the actions.",
|
||||
),
|
||||
explain(
|
||||
"Read the navigation scope",
|
||||
"Queries inspect bounded snapshots of currently loaded drafts, without applying them or loading missing included files. The panel identifies partial scope, draft evidence, detached or unverified files, and alias or ID collisions. Counts show total, displayed and omitted rows. A missing result in partial scope does not establish that the whole project has no match. If sources change, stale rows are disabled; run Definition or References again. Row locations use one-based lines and UTF-8 byte columns. Navigation preserves your source drafts and the accepted model.",
|
||||
),
|
||||
explain(
|
||||
"Find the record in diagrams",
|
||||
"Choose Show in diagrams from the Text toolbar on a unique verified alias in clean sources, or from the chosen model-tree record's context menu. Choose a diagram destination, then an exact representation. Results follow visible projection: ports, state rows, repeated caller tiles and their drawn sequence links remain distinct. Hidden tiles and nested links that are not drawn are excluded. A direct allocation can be represented by visible grouping; aggregate summaries do not invent allocation relations. A visual marker locates the port, row or grouped work while keeping the queried semantic record selected for model actions. Moving the camera preserves that marker. Finish pending edits first; navigation preserves accepted geometry, sources and history.",
|
||||
),
|
||||
explain(
|
||||
"Names, aliases and IDs serve different purposes",
|
||||
"The displayed name is engineering prose. A stable @alias is the readable source reference and need not change when the name changes. UUID identity distinguishes the persistent record. Preserve IDs when revising the same engineering object; changing identity is a deliberate operation that affects references.",
|
||||
|
||||
@@ -7,6 +7,7 @@ mod context_menu;
|
||||
mod data_values;
|
||||
mod data_view;
|
||||
mod diagram;
|
||||
mod diagram_usage;
|
||||
mod document;
|
||||
mod explorer;
|
||||
mod explorer_view;
|
||||
|
||||
@@ -281,6 +281,11 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
}
|
||||
entries.push(item("Properties", Message::EditProperties(id), true));
|
||||
entries.push(item("Open source", Message::SourceForSelection, true));
|
||||
entries.push(item(
|
||||
"Show in diagrams…",
|
||||
app.diagram_usage_message(id),
|
||||
true,
|
||||
));
|
||||
entries.push(item(
|
||||
"Connections & references",
|
||||
Message::InspectorTab(crate::navigation::InspectorTab::Connections),
|
||||
|
||||
@@ -126,28 +126,8 @@ fn groups(p: &Project, selected: Id) -> Vec<Group> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn diagrams_containing(p: &Project, selected: Id) -> Vec<Id> {
|
||||
let endpoints = p.relations.get(&selected).map(|r| [r.source(), r.target()]);
|
||||
p.diagrams
|
||||
.values()
|
||||
.filter(|d| {
|
||||
let placed = |id| {
|
||||
let e = p.elements.get(&id);
|
||||
let owner = e
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.and_then(|e| e.parent)
|
||||
.unwrap_or(id);
|
||||
let kind = p.elements.get(&owner).map(|e| e.kind());
|
||||
kind.is_none_or(|kind| d.projection.includes(kind))
|
||||
&& d.nodes.values().any(|n| n.element == owner || n.id == id)
|
||||
};
|
||||
placed(selected) || endpoints.is_some_and(|ends| ends.into_iter().all(placed))
|
||||
})
|
||||
.map(|d| d.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn view(p: &Project, selected: Id) -> Element<'_, Message> {
|
||||
pub fn view(app: &crate::app::App, selected: Id) -> Element<'_, Message> {
|
||||
let p = app.project();
|
||||
let mut content = column![
|
||||
text(short(p.named(selected), 50)).size(18),
|
||||
text("Follow ownership, usage and engineering relationships.")
|
||||
@@ -203,25 +183,8 @@ pub fn view(p: &Project, selected: Id) -> Element<'_, Message> {
|
||||
content = content.push(container(entry).padding(8).style(style::subtle));
|
||||
}
|
||||
}
|
||||
let diagrams = diagrams_containing(p, selected);
|
||||
if !diagrams.is_empty() {
|
||||
content = content.push(label("SHOWN IN DIAGRAMS"));
|
||||
for id in diagrams {
|
||||
content = content.push(
|
||||
action(
|
||||
&short(p.named(id), 42),
|
||||
Message::RevealInDiagram(id, selected),
|
||||
)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
} else if p.elements.contains_key(&selected) {
|
||||
content = content.push(
|
||||
text("This element has no diagram placement yet.")
|
||||
.size(12)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
content =
|
||||
content.push(action("Show in diagrams…", app.diagram_usage_message(selected)).width(Fill));
|
||||
if count == 0 {
|
||||
content = content.push(text("No semantic connections yet. Use Actions or right-click an element to add relationships.").size(12).color(style::MUTED));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
//! Literal source search and line navigation, independent of accepted model state.
|
||||
mod controller;
|
||||
pub mod interaction;
|
||||
pub mod query;
|
||||
pub mod symbols;
|
||||
pub use controller::*;
|
||||
use iced::widget::text_editor::{Cursor, Position};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Ephemeral editor navigation; no model commands or filesystem operations.
|
||||
use super::query;
|
||||
use iced::widget::text_editor::{Cursor, Position};
|
||||
use oa_core::Id;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SymbolEvent {
|
||||
DiagramUsages,
|
||||
Query(query::Action),
|
||||
Back,
|
||||
Close,
|
||||
Choose {
|
||||
stamp: query::QueryStamp,
|
||||
reference: bool,
|
||||
index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Identity of the mounted source view that emitted an action. Message queues
|
||||
/// may outlive that view; request-time stamping alone cannot detect replay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SymbolContext {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistoryEntry {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
pub generation: u64,
|
||||
pub cursor: Cursor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request {
|
||||
pub job: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub stamp: query::QueryStamp,
|
||||
pub path: String,
|
||||
pub offset: usize,
|
||||
pub cursor: Cursor,
|
||||
pub action: query::Action,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub request: Request,
|
||||
pub result: Result<(Arc<query::Snapshot>, Arc<query::Results>), String>,
|
||||
}
|
||||
|
||||
pub struct Reveal {
|
||||
pub ticket: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub snapshot: query::SnapshotKey,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct SymbolState {
|
||||
pub open: bool,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: Arc<AtomicU64>,
|
||||
pub serial: u64,
|
||||
pub in_flight: Option<Id>,
|
||||
pub request: Option<Request>,
|
||||
pub cache: Option<Arc<query::Snapshot>>,
|
||||
pub results: Option<Arc<query::Results>>,
|
||||
pub notice: Option<String>,
|
||||
pub back: Vec<HistoryEntry>,
|
||||
pub reveal: Option<Reveal>,
|
||||
}
|
||||
|
||||
impl Default for SymbolState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
interaction: Id::new_v4(),
|
||||
raw_interaction: Arc::new(AtomicU64::new(0)),
|
||||
serial: 0,
|
||||
in_flight: None,
|
||||
request: None,
|
||||
cache: None,
|
||||
results: None,
|
||||
notice: None,
|
||||
back: Vec::new(),
|
||||
reveal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iced uses zero-based UTF-8 byte columns. Reject stale or split-codepoint
|
||||
/// coordinates; do not normalize CRLF or silently clamp query destinations.
|
||||
pub fn byte_offset(text: &str, position: Position) -> Option<usize> {
|
||||
let start = if position.line == 0 {
|
||||
0
|
||||
} else {
|
||||
text.match_indices('\n').nth(position.line - 1)?.0 + 1
|
||||
};
|
||||
let end = text[start..]
|
||||
.find('\n')
|
||||
.map_or(text.len(), |end| start + end);
|
||||
let offset = start.checked_add(position.column)?;
|
||||
(offset <= end && text.is_char_boundary(offset)).then_some(offset)
|
||||
}
|
||||
|
||||
pub fn cursor_offset(text: &str, cursor: Cursor) -> Option<usize> {
|
||||
let position = cursor.selection.map_or(cursor.position, |selection| {
|
||||
if (selection.line, selection.column) < (cursor.position.line, cursor.position.column) {
|
||||
selection
|
||||
} else {
|
||||
cursor.position
|
||||
}
|
||||
});
|
||||
byte_offset(text, position)
|
||||
}
|
||||
|
||||
pub fn exact_cursor(text: &str, cursor: Cursor) -> bool {
|
||||
byte_offset(text, cursor.position).is_some()
|
||||
&& cursor
|
||||
.selection
|
||||
.is_none_or(|position| byte_offset(text, position).is_some())
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Raw input invalidation and focus checks at widget-operation execution time.
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget};
|
||||
use iced::{Element, Event, Length, Rectangle, Size, Task, Vector, keyboard, mouse, touch};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
/// Exhaustion fails closed: a saturated clock can never authorize navigation.
|
||||
/// Replace the clock with a new workspace/controller, rather than wrapping it.
|
||||
pub fn is_current(clock: &AtomicU64, expected: u64) -> bool {
|
||||
expected != u64::MAX && clock.load(Ordering::Acquire) == expected
|
||||
}
|
||||
|
||||
fn invalidate(clock: &AtomicU64, event: &Event) {
|
||||
if matches!(
|
||||
event,
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { .. })
|
||||
| Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
|
||||
| Event::Touch(touch::Event::FingerPressed { .. })
|
||||
| Event::Window(iced::window::Event::Unfocused)
|
||||
) {
|
||||
let _ = clock.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
|
||||
Some(value.saturating_add(1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent, including overlays: invalidate before the child can capture input.
|
||||
pub fn observe<'a, Message: 'a, Theme: 'a, Renderer: renderer::Renderer + 'a>(
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(Observed { content, clock })
|
||||
}
|
||||
|
||||
struct Observed<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> Widget<Message, Theme, Renderer>
|
||||
for Observed<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
}
|
||||
|
||||
struct ObservedOverlay<'a, Message, Theme, Renderer> {
|
||||
content: overlay::Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for ObservedOverlay<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
self.content.as_overlay_mut().layout(renderer, bounds)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.draw(renderer, theme, style, layout, cursor);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.operate(layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.mouse_interaction(layout, cursor, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.overlay(layout, renderer)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
fn index(&self) -> f32 {
|
||||
self.content.as_overlay().index()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect all focus owners, then focus only the mounted target. Iced executes
|
||||
/// Operation::Chain synchronously, without an intervening input event. Both
|
||||
/// traversals recheck the raw clock; no result message authorizes later focus.
|
||||
pub fn guarded_focus<Message: Send + 'static>(
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
) -> Task<Message> {
|
||||
widget::operate(GuardedFocus::new(target, clock, expected))
|
||||
}
|
||||
|
||||
struct GuardedFocus {
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
target_count: usize,
|
||||
foreign_focus: bool,
|
||||
apply: bool,
|
||||
}
|
||||
impl GuardedFocus {
|
||||
fn new(target: widget::Id, clock: Arc<AtomicU64>, expected: u64) -> Self {
|
||||
Self {
|
||||
target,
|
||||
clock,
|
||||
expected,
|
||||
target_count: 0,
|
||||
foreign_focus: false,
|
||||
apply: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Message: 'static> widget::Operation<Message> for GuardedFocus {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation<Message>)) {
|
||||
if is_current(&self.clock, self.expected) {
|
||||
visit(self);
|
||||
}
|
||||
}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
) {
|
||||
if !is_current(&self.clock, self.expected) {
|
||||
return;
|
||||
}
|
||||
if id == Some(&self.target) {
|
||||
self.target_count += 1;
|
||||
if self.apply {
|
||||
state.focus();
|
||||
}
|
||||
} else if state.is_focused() {
|
||||
self.foreign_focus = true;
|
||||
}
|
||||
}
|
||||
fn finish(&self) -> widget::operation::Outcome<Message> {
|
||||
if !self.apply
|
||||
&& self.target_count == 1
|
||||
&& !self.foreign_focus
|
||||
&& is_current(&self.clock, self.expected)
|
||||
{
|
||||
widget::operation::Outcome::Chain(Box::new(Self {
|
||||
apply: true,
|
||||
..Self::new(self.target.clone(), self.clock.clone(), self.expected)
|
||||
}))
|
||||
} else {
|
||||
widget::operation::Outcome::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests;
|
||||
@@ -0,0 +1,190 @@
|
||||
use super::*;
|
||||
use iced::advanced::widget::operation::{Focusable, Outcome};
|
||||
use iced::widget::{column, text_input};
|
||||
use iced::{Point, Theme};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FocusMap(Vec<(Option<widget::Id>, Rectangle, bool)>);
|
||||
impl widget::Operation for FocusMap {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn focusable(&mut self, id: Option<&widget::Id>, bounds: Rectangle, state: &mut dyn Focusable) {
|
||||
self.0.push((id.cloned(), bounds, state.is_focused()));
|
||||
}
|
||||
}
|
||||
|
||||
struct Mounted {
|
||||
element: Element<'static, (), Theme, ()>,
|
||||
tree: widget::Tree,
|
||||
node: layout::Node,
|
||||
}
|
||||
impl Mounted {
|
||||
fn new(clock: Arc<AtomicU64>) -> Self {
|
||||
let content = column![
|
||||
text_input("Editor", "source")
|
||||
.id("editor")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
text_input("Find", "")
|
||||
.id("find")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
]
|
||||
.spacing(10)
|
||||
.into();
|
||||
let mut element = observe(content, clock);
|
||||
let mut tree = widget::Tree::new(element.as_widget());
|
||||
let node = element.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(400., 200.)),
|
||||
);
|
||||
Self {
|
||||
element,
|
||||
tree,
|
||||
node,
|
||||
}
|
||||
}
|
||||
fn pass(&mut self, operation: &mut dyn widget::Operation) {
|
||||
self.element.as_widget_mut().operate(
|
||||
&mut self.tree,
|
||||
Layout::new(&self.node),
|
||||
&(),
|
||||
operation,
|
||||
);
|
||||
}
|
||||
fn run(&mut self, operation: impl widget::Operation + 'static) {
|
||||
let mut next: Box<dyn widget::Operation> = Box::new(operation);
|
||||
loop {
|
||||
self.pass(next.as_mut());
|
||||
match next.finish() {
|
||||
Outcome::Chain(chained) => next = chained,
|
||||
Outcome::None => break,
|
||||
Outcome::Some(()) => panic!("focus must not publish an application message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn focus(&mut self) -> FocusMap {
|
||||
let mut focus = FocusMap::default();
|
||||
self.pass(&mut focus);
|
||||
focus
|
||||
}
|
||||
fn click(&mut self, id: &str) {
|
||||
let bounds = self
|
||||
.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.1;
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
self.element.as_widget_mut().update(
|
||||
&mut self.tree,
|
||||
&Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Layout::new(&self.node),
|
||||
mouse::Cursor::Available(bounds.center()),
|
||||
&(),
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(Size::new(400., 200.)),
|
||||
);
|
||||
assert!(shell.is_event_captured());
|
||||
assert!(
|
||||
messages.is_empty(),
|
||||
"focus-only click must not rely on App messages"
|
||||
);
|
||||
}
|
||||
fn focused(&mut self, id: &str) -> bool {
|
||||
self.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.2
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_find_click_invalidates_without_app_message_and_preserves_focus() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.click("find");
|
||||
assert!(!is_current(&clock, 0));
|
||||
assert!(ui.focused("find"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("find"));
|
||||
// Even a matching clock cannot authorize stealing another widget's focus.
|
||||
ui.run(GuardedFocus::new(
|
||||
"editor".into(),
|
||||
clock.clone(),
|
||||
clock.load(Ordering::Acquire),
|
||||
));
|
||||
assert!(ui.focused("find"));
|
||||
assert!(!ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_rechecks_clock_between_inspection_and_application_and_missing_target_is_inert() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
let mut operation = GuardedFocus::new("editor".into(), clock.clone(), 0);
|
||||
ui.pass(&mut operation);
|
||||
let Outcome::Chain(mut apply) = <GuardedFocus as widget::Operation<()>>::finish(&operation)
|
||||
else {
|
||||
panic!("mounted, unopposed target must be eligible");
|
||||
};
|
||||
clock.store(1, Ordering::Release);
|
||||
ui.pass(apply.as_mut());
|
||||
assert!(!ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 1));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("removed-editor".into(), clock, 1));
|
||||
assert!(ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_navigation_inputs_advance_but_passive_geometry_does_not_and_exhaustion_refuses() {
|
||||
let clock = AtomicU64::new(0);
|
||||
let events = [
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::ArrowLeft),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
}),
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
||||
}),
|
||||
Event::Touch(touch::Event::FingerPressed {
|
||||
id: touch::Finger(1),
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
Event::Window(iced::window::Event::Unfocused),
|
||||
];
|
||||
for (index, event) in events.iter().enumerate() {
|
||||
invalidate(&clock, event);
|
||||
assert_eq!(clock.load(Ordering::Acquire), index as u64 + 1);
|
||||
}
|
||||
invalidate(
|
||||
&clock,
|
||||
&Event::Mouse(mouse::Event::CursorMoved {
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
);
|
||||
invalidate(&clock, &Event::Window(iced::window::Event::Focused));
|
||||
assert!(is_current(&clock, 5));
|
||||
clock.store(u64::MAX - 1, Ordering::Release);
|
||||
invalidate(&clock, &events[0]);
|
||||
assert!(!is_current(&clock, u64::MAX));
|
||||
invalidate(&clock, &events[0]);
|
||||
assert_eq!(clock.load(Ordering::Acquire), u64::MAX);
|
||||
assert!(!is_current(&clock, 0));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,989 @@
|
||||
use super::*;
|
||||
|
||||
const PROJECT: &str = "00000000-0000-0000-0000-000000000001";
|
||||
const PUMP: &str = "00000000-0000-0000-0000-000000000002";
|
||||
const LINK: &str = "00000000-0000-0000-0000-000000000003";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Fixture {
|
||||
entry: String,
|
||||
files: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn standard() -> Self {
|
||||
Self {
|
||||
entry: "project.arcadia".into(),
|
||||
files: vec![
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 7;\r\nproject model \"Harbour\" id \"{PROJECT}\" {{\r\n include \"模块/活動.arcadia\";\r\n include \"links.arcadia\";\r\n}}\r\n"
|
||||
),
|
||||
),
|
||||
(
|
||||
"模块/活動.arcadia".into(),
|
||||
format!(
|
||||
"// 原文 remains exact\r\noa_activity pump \"泵 🚀\" id \"{PUMP}\" {{}}\r\n"
|
||||
),
|
||||
),
|
||||
(
|
||||
"links.arcadia".into(),
|
||||
format!(
|
||||
"// 航海\r\ninteraction connection \"流れ\" id \"{LINK}\" {{\r\n source @ // separator @pump\r\n pump;\r\n description \"船 @pump\"; target @pump;\r\n}}\r\n"
|
||||
),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn borrowed(&self) -> Vec<DraftFile<'_>> {
|
||||
self.files
|
||||
.iter()
|
||||
.map(|(path, text)| DraftFile { path, text })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text(&self, path: &str) -> &str {
|
||||
&self
|
||||
.files
|
||||
.iter()
|
||||
.find(|(candidate, _)| candidate == path)
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
fn text_mut(&mut self, path: &str) -> &mut String {
|
||||
&mut self
|
||||
.files
|
||||
.iter_mut()
|
||||
.find(|(candidate, _)| candidate == path)
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
fn snapshot(&self, validated: Option<&Self>, applied: bool, limits: Limits) -> Snapshot {
|
||||
let current = self.borrowed();
|
||||
let old = validated.map(Self::borrowed);
|
||||
capture(
|
||||
Input {
|
||||
key: key(1),
|
||||
entry: &self.entry,
|
||||
files: ¤t,
|
||||
applied,
|
||||
validation: old.as_ref().map(|files| ValidatedSources {
|
||||
entry: &validated.unwrap().entry,
|
||||
files,
|
||||
}),
|
||||
},
|
||||
limits,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn key(revision: u64) -> SnapshotKey {
|
||||
SnapshotKey {
|
||||
workspace: Id::from_u128(100),
|
||||
revision,
|
||||
}
|
||||
}
|
||||
|
||||
fn query(snapshot: &Snapshot, path: &str, byte_offset: usize, action: Action) -> Results {
|
||||
snapshot
|
||||
.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: snapshot.key(),
|
||||
serial: 37,
|
||||
},
|
||||
path,
|
||||
byte_offset,
|
||||
action,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn reference_offset(fixture: &Fixture) -> usize {
|
||||
fixture.text("links.arcadia").find("source @").unwrap() + "source ".len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_multifile_unicode_crlf_definition_and_repeated_lexical_references() {
|
||||
let fixture = Fixture::standard();
|
||||
let original = fixture.files.clone();
|
||||
let snapshot = fixture.snapshot(Some(&fixture), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete && snapshot.scope().validated);
|
||||
assert_eq!(
|
||||
(
|
||||
snapshot.scope().included_files,
|
||||
snapshot.scope().detached_files
|
||||
),
|
||||
(3, 0)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Direct);
|
||||
assert_eq!(
|
||||
(result.total_definitions, result.omitted_definitions),
|
||||
(1, 0)
|
||||
);
|
||||
let definition = &result.definitions[0];
|
||||
assert_eq!(definition.path, "模块/活動.arcadia");
|
||||
assert_eq!(definition.declaration.kind, "oa_activity");
|
||||
assert_eq!(definition.declaration.name.as_deref(), Some("泵 🚀"));
|
||||
assert_eq!(
|
||||
definition.declaration.authored_id,
|
||||
Some(Id::parse_str(PUMP).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
definition.position,
|
||||
Position {
|
||||
line: 1,
|
||||
byte_column: "oa_activity ".len()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
&fixture.text(&definition.path)[definition.span.start..definition.span.end],
|
||||
"pump"
|
||||
);
|
||||
let uses = query(
|
||||
&snapshot,
|
||||
&definition.path,
|
||||
definition.span.start,
|
||||
Action::References,
|
||||
);
|
||||
assert_eq!(uses.disposition, Disposition::LexicalReferences);
|
||||
assert_eq!((uses.total_references, uses.omitted_references), (2, 0));
|
||||
assert_eq!(
|
||||
uses.references[0].position,
|
||||
Position {
|
||||
line: 3,
|
||||
byte_column: 3
|
||||
}
|
||||
);
|
||||
let target_prefix = " description \"船 @pump\"; target @";
|
||||
assert_ne!(target_prefix.len(), target_prefix.chars().count());
|
||||
assert_eq!(
|
||||
uses.references[1].position,
|
||||
Position {
|
||||
line: 4,
|
||||
byte_column: target_prefix.len()
|
||||
}
|
||||
);
|
||||
for row in &uses.references {
|
||||
let text = fixture.text(&row.path);
|
||||
assert_eq!(&text[row.span.start..row.span.end], "pump");
|
||||
assert_eq!(&text[row.marker.start..row.marker.end], "@");
|
||||
let context = row.context.as_ref().unwrap();
|
||||
assert_eq!(context.alias, "connection");
|
||||
assert_eq!(context.kind, "interaction");
|
||||
assert_eq!(context.name.as_deref(), Some("流れ"));
|
||||
assert_eq!(row.evidence, Evidence::Validated);
|
||||
}
|
||||
assert_eq!(
|
||||
uses.stamp,
|
||||
QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 37
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
fixture.files, original,
|
||||
"no stabilization, newline rewrite or accepted adoption"
|
||||
);
|
||||
for (index, (path, text)) in original.iter().enumerate() {
|
||||
let owned = snapshot.file(index).unwrap();
|
||||
assert_eq!((owned.path, owned.text), (path.as_str(), text.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_strings_and_comment_gap_are_not_navigation_hits() {
|
||||
let fixture = Fixture::standard();
|
||||
let snapshot = fixture.snapshot(None, true, Limits::default());
|
||||
let text = fixture.text("links.arcadia");
|
||||
for offset in [
|
||||
text.find("separator @pump").unwrap() + "separator ".len(),
|
||||
text.find("船 @pump").unwrap() + "船 ".len(),
|
||||
text.find("@ //").unwrap() + 1,
|
||||
] {
|
||||
let result = query(&snapshot, "links.arcadia", offset, Action::Definition);
|
||||
assert_eq!(result.disposition, Disposition::NoSymbol);
|
||||
assert_eq!(result.total_definitions, 0);
|
||||
}
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
text.find(" pump;").unwrap() + 3,
|
||||
Action::Definition
|
||||
)
|
||||
.total_definitions,
|
||||
1
|
||||
);
|
||||
let inside_utf8 = text.find('船').unwrap() + 1;
|
||||
assert_eq!(
|
||||
snapshot.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 1
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: inside_utf8,
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::Query(symbols::QueryError::InvalidBytePosition))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_but_parseable_text_has_current_names_and_no_validated_fallback() {
|
||||
let original = Fixture::standard();
|
||||
let mut dirty = original.clone();
|
||||
*dirty.text_mut("模块/活動.arcadia") = dirty
|
||||
.text("模块/活動.arcadia")
|
||||
.replace("泵 🚀", "Edited 泵\\nwith detail");
|
||||
let snapshot = dirty.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete);
|
||||
assert!(!snapshot.scope().validated);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&dirty),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.name.as_deref(),
|
||||
Some("Edited 泵\nwith detail")
|
||||
);
|
||||
assert_eq!(result.definitions[0].evidence, Evidence::Draft);
|
||||
assert_eq!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
dirty.text("模块/活動.arcadia")
|
||||
);
|
||||
assert_ne!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
original.text("模块/活動.arcadia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_include_syntax_never_borrows_the_old_reachable_file_set() {
|
||||
let original = Fixture::standard();
|
||||
let mut invalid = original.clone();
|
||||
*invalid.text_mut("project.arcadia") = invalid
|
||||
.text("project.arcadia")
|
||||
.replace("include \"links.arcadia\";", "include \"links.arcadia\"");
|
||||
let snapshot = invalid.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(!snapshot.scope().validated);
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::InvalidIncludes)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&invalid),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(result.definitions[0].membership, Membership::Unverified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_include_partial_scope_does_not_resurrect_witness_text() {
|
||||
let original = Fixture::standard();
|
||||
let mut missing = original.clone();
|
||||
missing
|
||||
.files
|
||||
.retain(|(path, _)| path != "模块/活動.arcadia");
|
||||
let snapshot = missing.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::MissingInclude)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&missing),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::NoDefinition);
|
||||
assert!(result.definitions.is_empty());
|
||||
assert!(
|
||||
result.scope.partial(),
|
||||
"zero means no loaded definition, not no project definition"
|
||||
);
|
||||
assert!(snapshot.file(2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_alias_and_identity_collisions_never_auto_select() {
|
||||
let accepted = Fixture::standard();
|
||||
for (alias, id) in [
|
||||
("pump", "00000000-0000-0000-0000-000000000099"),
|
||||
("other", PUMP),
|
||||
] {
|
||||
let mut loaded = accepted.clone();
|
||||
loaded.files.push((
|
||||
"detached.arcadia".into(),
|
||||
format!("oa_activity {alias} \"Detached\" id \"{id}\" {{}}"),
|
||||
));
|
||||
let snapshot = loaded.snapshot(Some(&accepted), true, Limits::default());
|
||||
assert!(snapshot.scope().validated);
|
||||
assert_eq!(snapshot.scope().detached_files, 1);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&loaded),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
if alias == "pump" {
|
||||
assert_eq!(result.total_definitions, 2);
|
||||
assert!(result.definitions.iter().any(
|
||||
|row| row.membership == Membership::Detached && row.evidence == Evidence::Draft
|
||||
));
|
||||
} else {
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.identity,
|
||||
Identity::Collision { count: 2 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sole_detached_candidate_and_unapplied_equal_bytes_require_choice() {
|
||||
let accepted = Fixture::standard();
|
||||
let mut detached = accepted.clone();
|
||||
*detached.text_mut("project.arcadia") = detached
|
||||
.text("project.arcadia")
|
||||
.replace(" include \"模块/活動.arcadia\";\r\n", "");
|
||||
let snapshot = detached.snapshot(None, true, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&detached),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(result.definitions[0].membership, Membership::Detached);
|
||||
let unapplied = accepted.snapshot(Some(&accepted), false, Limits::default());
|
||||
assert!(unapplied.scope().closure_complete && unapplied.scope().partial());
|
||||
assert!(!unapplied.scope().validated);
|
||||
assert_eq!(
|
||||
query(
|
||||
&unapplied,
|
||||
"links.arcadia",
|
||||
reference_offset(&accepted),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn include_cycle_depth_and_aggregate_edges_are_explicit_partial_outcomes() {
|
||||
let mut fixture = Fixture::standard();
|
||||
fixture
|
||||
.text_mut("模块/活動.arcadia")
|
||||
.push_str("include \"../project.arcadia\";\r\n");
|
||||
let cycle = fixture.snapshot(None, true, Limits::default());
|
||||
assert!(
|
||||
cycle
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::IncludeCycle)
|
||||
);
|
||||
let fixture = Fixture::standard();
|
||||
for (depth, edges, expected) in [
|
||||
(0, 1024, ScopeIssueKind::IncludeDepth),
|
||||
(32, 1, ScopeIssueKind::IncludeBudget),
|
||||
] {
|
||||
let snapshot = fixture.snapshot(
|
||||
None,
|
||||
true,
|
||||
Limits {
|
||||
include_depth: depth,
|
||||
include_edges: edges,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == expected)
|
||||
);
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_snapshot_readback_rejects_same_key_byte_membership_and_evidence_changes() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let validation = Some(ValidatedSources {
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
});
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation,
|
||||
};
|
||||
let snapshot = capture(input, Limits::default()).unwrap();
|
||||
assert!(snapshot.matches(input));
|
||||
assert!(!snapshot.matches(Input {
|
||||
key: key(2),
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
validation: None,
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
applied: false,
|
||||
..input
|
||||
}));
|
||||
let mut reversed = files.clone();
|
||||
reversed.reverse();
|
||||
assert!(snapshot.matches(Input {
|
||||
files: &reversed,
|
||||
..input
|
||||
}));
|
||||
let mut changed = fixture.clone();
|
||||
changed.text_mut("links.arcadia").push(' ');
|
||||
let altered = changed.borrowed();
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &altered,
|
||||
..input
|
||||
}));
|
||||
let missing = &files[..files.len() - 1];
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: missing,
|
||||
..input
|
||||
}));
|
||||
let duplicated = [files[0], files[0], files[2]];
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &duplicated,
|
||||
..input
|
||||
}));
|
||||
assert_eq!(
|
||||
snapshot.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(2),
|
||||
serial: 37
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: 0,
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::Query(symbols::QueryError::StaleSnapshot))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_rows_are_counted_before_result_allocation_and_never_confer_uniqueness() {
|
||||
let fixture = Fixture::standard();
|
||||
let snapshot = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_rows: 2,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References,
|
||||
);
|
||||
assert_eq!((result.total_definitions, result.total_references), (1, 2));
|
||||
assert_eq!(
|
||||
(
|
||||
result.definitions.len(),
|
||||
result.references.len(),
|
||||
result.omitted_references
|
||||
),
|
||||
(1, 1, 1)
|
||||
);
|
||||
let zero = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_rows: 0,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&zero,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
result.definitions.len(),
|
||||
result.total_definitions,
|
||||
result.omitted_definitions
|
||||
),
|
||||
(0, 1, 1)
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
let envelope = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_bytes: 0,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
envelope.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 1
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: reference_offset(&fixture),
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::ResultEnvelopeLimit)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_display_name_cannot_escape_the_owned_row_string_budget() {
|
||||
let mut fixture = Fixture::standard();
|
||||
*fixture.text_mut("模块/活動.arcadia") = fixture
|
||||
.text("模块/活動.arcadia")
|
||||
.replace("泵 🚀", &"長".repeat(8192));
|
||||
let original = fixture.files.clone();
|
||||
let snapshot = fixture.snapshot(
|
||||
None,
|
||||
true,
|
||||
Limits {
|
||||
result_bytes: 64,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(
|
||||
(result.total_definitions, result.omitted_definitions),
|
||||
(1, 1)
|
||||
);
|
||||
assert!(result.definitions.is_empty());
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(fixture.files, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_witness_must_match_entry_and_complete_included_file_set() {
|
||||
let fixture = Fixture::standard();
|
||||
for mutation in 0..3 {
|
||||
let mut witness = fixture.clone();
|
||||
match mutation {
|
||||
0 => witness.entry = "other.arcadia".into(),
|
||||
1 => {
|
||||
witness.files.pop();
|
||||
}
|
||||
_ => witness.files.push(("unused.arcadia".into(), String::new())),
|
||||
}
|
||||
let snapshot = fixture.snapshot(Some(&witness), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete);
|
||||
assert!(!snapshot.scope().validated);
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_aggregate_limits_refuse_before_outline_or_copied_snapshot() {
|
||||
// Token budget zero and malformed paths/text would fail later. Raw byte/path
|
||||
// limits must win, including a large second file; no partial Snapshot exists.
|
||||
let large = "not a model \0".repeat(16_384);
|
||||
let files = [
|
||||
DraftFile {
|
||||
path: "../bad.arcadia",
|
||||
text: "?",
|
||||
},
|
||||
DraftFile {
|
||||
path: "large.arcadia",
|
||||
text: &large,
|
||||
},
|
||||
];
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: "project.arcadia",
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None,
|
||||
};
|
||||
for (resource, files_limit, bytes_limit, paths_limit) in [
|
||||
(symbols::Resource::Files, 1, usize::MAX, usize::MAX),
|
||||
(symbols::Resource::SourceBytes, 8, 1, usize::MAX),
|
||||
(symbols::Resource::PathBytes, 8, usize::MAX, 1),
|
||||
] {
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
files: files_limit,
|
||||
source_bytes: bytes_limit,
|
||||
path_bytes: paths_limit,
|
||||
tokens: 0,
|
||||
records: 0,
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(capture(input, limits), Err(AdmissionError::Index(symbols::BuildError::Limit { resource: actual, .. })) if actual == resource)
|
||||
);
|
||||
}
|
||||
assert_eq!(files[1].text, large);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_declaration_and_live_reference_limits_are_aggregate_not_per_file() {
|
||||
let files = [
|
||||
DraftFile {
|
||||
path: "a.arcadia",
|
||||
text: "oa_activity a \"A\" {}",
|
||||
},
|
||||
DraftFile {
|
||||
path: "b.arcadia",
|
||||
text: "oa_activity b \"B\" {}",
|
||||
},
|
||||
];
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: "a.arcadia",
|
||||
files: &files,
|
||||
applied: false,
|
||||
validation: None,
|
||||
};
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
tokens: 10,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(input, limits),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 1,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Tokens,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
records: 1,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(input, limits),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 1,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Declarations,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let use_files = [
|
||||
files[0],
|
||||
DraftFile {
|
||||
path: "uses.arcadia",
|
||||
text: "source @a; target @a; // @a ignored\n",
|
||||
},
|
||||
];
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
records: 2,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(
|
||||
Input {
|
||||
files: &use_files,
|
||||
..input
|
||||
},
|
||||
limits
|
||||
),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Limit {
|
||||
resource: symbols::Resource::Records,
|
||||
observed_at_least: 3,
|
||||
..
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_and_noncanonical_authored_ids_remain_draft_spans_without_generated_anchors() {
|
||||
let fixture = Fixture::standard();
|
||||
for replacement in [
|
||||
String::new(),
|
||||
" id \"00000000000000000000000000000002\"".into(),
|
||||
] {
|
||||
let mut draft = fixture.clone();
|
||||
let original_id = format!(" id \"{PUMP}\"");
|
||||
*draft.text_mut("模块/活動.arcadia") = draft
|
||||
.text("模块/活動.arcadia")
|
||||
.replace(&original_id, &replacement);
|
||||
draft
|
||||
.text_mut("模块/活動.arcadia")
|
||||
.push_str(&format!("// Historical UUID {PUMP} and @pump\r\n"));
|
||||
let before = draft.files.clone();
|
||||
let snapshot = draft.snapshot(None, false, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&draft),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.definitions[0].declaration.authored_id, None);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.identity,
|
||||
Identity::Missing
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(draft.files, before);
|
||||
assert_eq!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
draft.text("模块/活動.arcadia")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyword_alias_and_loaded_path_case_collisions_use_reviewed_index_rules() {
|
||||
let mut fixture = Fixture::standard();
|
||||
for (_, text) in &mut fixture.files {
|
||||
*text = text.replace("pump", "source");
|
||||
}
|
||||
let snapshot = fixture.snapshot(None, true, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.alias.as_deref(), Some("source"));
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
fixture.files.push(("LINKS.arcadia".into(), String::new()));
|
||||
let files = fixture.borrowed();
|
||||
assert!(matches!(
|
||||
capture(
|
||||
Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None
|
||||
},
|
||||
Limits::default()
|
||||
),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Input {
|
||||
problem: symbols::InputProblem::DuplicateOrCaseCollidingPath,
|
||||
..
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_request_moves_exact_text_into_worker_snapshot_and_matches_sync_results() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: Some(ValidatedSources {
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
}),
|
||||
};
|
||||
let owned = admit_owned(input, Limits::default()).unwrap();
|
||||
let allocations: Vec<_> = owned.files.iter().map(|file| file.text.as_ptr()).collect();
|
||||
let snapshot = owned.capture().unwrap();
|
||||
for (file, allocation) in allocations.iter().enumerate() {
|
||||
assert_eq!(
|
||||
snapshot.file(file).unwrap().text.as_ptr(),
|
||||
*allocation,
|
||||
"worker moves the one current-text copy"
|
||||
);
|
||||
}
|
||||
assert!(snapshot.matches(input));
|
||||
let synchronous = capture(input, Limits::default()).unwrap();
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References
|
||||
),
|
||||
query(
|
||||
&synchronous,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References
|
||||
)
|
||||
);
|
||||
let mut edited = fixture.clone();
|
||||
edited
|
||||
.text_mut("links.arcadia")
|
||||
.push_str("// edited after request\r\n");
|
||||
let changed = edited.borrowed();
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &changed,
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
validation: None,
|
||||
..input
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_request_checks_raw_limits_immediately_and_extracts_only_on_worker() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None,
|
||||
};
|
||||
let no_tokens = Limits {
|
||||
index: symbols::Limits {
|
||||
tokens: 0,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
let owned = admit_owned(input, no_tokens).expect("request admission does not lex or parse");
|
||||
assert!(matches!(
|
||||
owned.capture(),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 0,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Tokens,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let no_bytes = Limits {
|
||||
index: symbols::Limits {
|
||||
source_bytes: 1,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
admit_owned(input, no_bytes),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Limit {
|
||||
resource: symbols::Resource::SourceBytes,
|
||||
..
|
||||
}))
|
||||
));
|
||||
assert_eq!(files[1].text, fixture.text("模块/活動.arcadia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_validation_proof_preserves_full_witness_count_and_partial_scope() {
|
||||
let accepted = Fixture::standard();
|
||||
for missing_include in [false, true] {
|
||||
let mut current = accepted.clone();
|
||||
current
|
||||
.files
|
||||
.retain(|(path, _)| path != "模块/活動.arcadia");
|
||||
if !missing_include {
|
||||
*current.text_mut("project.arcadia") = current
|
||||
.text("project.arcadia")
|
||||
.replace(" include \"模块/活動.arcadia\";\r\n", "");
|
||||
}
|
||||
let files = current.borrowed();
|
||||
let old = accepted.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: ¤t.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: Some(ValidatedSources {
|
||||
entry: &accepted.entry,
|
||||
files: &old,
|
||||
}),
|
||||
};
|
||||
let snapshot = admit_owned(input, Limits::default())
|
||||
.unwrap()
|
||||
.capture()
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.scope().closure_complete, !missing_include);
|
||||
assert!(
|
||||
!snapshot.scope().validated,
|
||||
"the old witness is not silently reduced to matching loaded files"
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(¤t),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::NoDefinition);
|
||||
assert!(snapshot.matches(input));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
//! Per-file drafts and navigation. Accepted model/source history lives in Document.
|
||||
use iced::widget::text_editor::Cursor;
|
||||
use oa_core::{
|
||||
language,
|
||||
Id, language,
|
||||
source_project::{self, CompiledSourceProject, ProjectLimits, SourceBundle, SourceDiagnostic},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -86,6 +86,7 @@ pub struct Buffer {
|
||||
pub cursor: Cursor,
|
||||
past: Vec<String>,
|
||||
future: Vec<String>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -99,14 +100,20 @@ impl Buffer {
|
||||
},
|
||||
past: Vec::new(),
|
||||
future: Vec::new(),
|
||||
generation: 0,
|
||||
}
|
||||
}
|
||||
pub fn dirty(&self) -> bool {
|
||||
self.text != self.baseline
|
||||
}
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Workspace {
|
||||
session: Id,
|
||||
revision: u64,
|
||||
pub entry: String,
|
||||
pub active: String,
|
||||
pub files: BTreeMap<String, Buffer>,
|
||||
@@ -117,6 +124,35 @@ pub struct Workspace {
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Ephemeral editor identity, never a model identity or recovery payload.
|
||||
pub fn session(&self) -> Id {
|
||||
self.session
|
||||
}
|
||||
pub fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
fn advance(&mut self) -> u64 {
|
||||
if let Some(revision) = self.revision.checked_add(1) {
|
||||
self.revision = revision;
|
||||
} else {
|
||||
self.reset_navigation_session();
|
||||
}
|
||||
self.revision
|
||||
}
|
||||
/// A changed filesystem root must not reuse queued navigation from Save As.
|
||||
pub fn reset_navigation_session(&mut self) {
|
||||
self.session = Id::new_v4();
|
||||
self.revision = 0;
|
||||
for buffer in self.files.values_mut() {
|
||||
buffer.generation = 0;
|
||||
}
|
||||
}
|
||||
pub fn set_reachable(&mut self, reachable: BTreeSet<String>) {
|
||||
if self.reachable != reachable {
|
||||
self.advance();
|
||||
self.reachable = reachable;
|
||||
}
|
||||
}
|
||||
pub fn recovery_snapshot(&self) -> RecoveryWorkspace {
|
||||
RecoveryWorkspace {
|
||||
entry: self.entry.clone(),
|
||||
@@ -136,6 +172,8 @@ impl Workspace {
|
||||
pub fn from_recovery(saved: RecoveryWorkspace) -> Result<Self, String> {
|
||||
saved.validate()?;
|
||||
Ok(Self {
|
||||
session: Id::new_v4(),
|
||||
revision: 0,
|
||||
entry: saved.entry,
|
||||
active: saved.active,
|
||||
files: saved
|
||||
@@ -154,6 +192,8 @@ impl Workspace {
|
||||
}
|
||||
pub fn new(bundle: &SourceBundle, applied: bool) -> Self {
|
||||
Self {
|
||||
session: Id::new_v4(),
|
||||
revision: 0,
|
||||
entry: bundle.entry.clone(),
|
||||
active: bundle.entry.clone(),
|
||||
files: bundle
|
||||
@@ -191,11 +231,20 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
pub fn edit(&mut self, text: String, cursor: Cursor) {
|
||||
let changed = self.files.get(&self.active).is_none_or(|b| b.text != text);
|
||||
let generation = if changed {
|
||||
self.advance()
|
||||
} else {
|
||||
self.revision
|
||||
};
|
||||
let buffer = self
|
||||
.files
|
||||
.entry(self.active.clone())
|
||||
.or_insert_with(|| Buffer::new(String::new()));
|
||||
buffer.cursor = cursor;
|
||||
if changed {
|
||||
buffer.generation = generation;
|
||||
}
|
||||
if text != buffer.text {
|
||||
buffer.past.push(std::mem::replace(&mut buffer.text, text));
|
||||
if buffer.past.len() > 100 {
|
||||
@@ -268,6 +317,8 @@ impl Workspace {
|
||||
} else {
|
||||
buffer.future.push(previous);
|
||||
}
|
||||
let generation = self.advance();
|
||||
self.files.get_mut(&self.active).unwrap().generation = generation;
|
||||
self.trim_history(DRAFT_HISTORY_BYTES);
|
||||
true
|
||||
}
|
||||
@@ -281,6 +332,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
pub fn accept(&mut self, bundle: &SourceBundle) {
|
||||
let generation = self.advance();
|
||||
if self.entry != bundle.entry
|
||||
&& !bundle.files.contains_key(&self.entry)
|
||||
&& self.files.get(&self.entry).is_some_and(|b| !b.dirty())
|
||||
@@ -291,7 +343,13 @@ impl Workspace {
|
||||
self.reachable = bundle.files.keys().cloned().collect();
|
||||
for (path, text) in &bundle.files {
|
||||
let cursor = self.files.get(path).map(|b| b.cursor);
|
||||
let previous_generation = self
|
||||
.files
|
||||
.get(path)
|
||||
.filter(|b| b.text == *text)
|
||||
.map(|b| b.generation);
|
||||
let mut buffer = Buffer::new(text.clone());
|
||||
buffer.generation = previous_generation.unwrap_or(generation);
|
||||
if let Some(cursor) = cursor {
|
||||
buffer.cursor = cursor;
|
||||
}
|
||||
@@ -303,10 +361,22 @@ impl Workspace {
|
||||
self.applied = true;
|
||||
}
|
||||
pub fn discovered(&mut self, bundle: &SourceBundle, reachable: BTreeSet<String>) {
|
||||
let changed = self.reachable != reachable
|
||||
|| bundle
|
||||
.files
|
||||
.keys()
|
||||
.any(|path| !self.files.contains_key(path));
|
||||
let generation = if changed {
|
||||
self.advance()
|
||||
} else {
|
||||
self.revision
|
||||
};
|
||||
for (path, text) in &bundle.files {
|
||||
self.files
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| Buffer::new(text.clone()));
|
||||
self.files.entry(path.clone()).or_insert_with(|| {
|
||||
let mut buffer = Buffer::new(text.clone());
|
||||
buffer.generation = generation;
|
||||
buffer
|
||||
});
|
||||
}
|
||||
self.reachable = reachable;
|
||||
}
|
||||
@@ -314,6 +384,7 @@ impl Workspace {
|
||||
if self.reachable.contains(path) || self.files.get(path).is_none_or(Buffer::dirty) {
|
||||
return false;
|
||||
}
|
||||
self.advance();
|
||||
self.files.remove(path);
|
||||
if self.active == path {
|
||||
self.active = self.entry.clone();
|
||||
@@ -376,6 +447,148 @@ pub fn lint(bundle: &SourceBundle) -> Vec<SourceDiagnostic> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use oa_core::Project;
|
||||
|
||||
#[test]
|
||||
fn fresh_session_cannot_reuse_a_retained_buffer_generation() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), "first".into()),
|
||||
("other.arcadia".into(), "other".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let cursor = workspace.files["main.arcadia"].cursor;
|
||||
workspace.edit("second".into(), cursor);
|
||||
workspace.reset_navigation_session();
|
||||
let before = workspace.files["main.arcadia"].generation();
|
||||
workspace.edit("third".into(), cursor);
|
||||
assert_ne!(workspace.files["main.arcadia"].generation(), before);
|
||||
workspace.switch("other.arcadia", cursor);
|
||||
workspace.revision = u64::MAX;
|
||||
workspace.edit("changed other".into(), cursor);
|
||||
let after_wrap = workspace.files["main.arcadia"].generation();
|
||||
workspace.switch("main.arcadia", cursor);
|
||||
workspace.edit("fourth".into(), cursor);
|
||||
assert_ne!(workspace.files["main.arcadia"].generation(), after_wrap);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.text(), "third");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_versions_distinguish_reopened_recovered_and_changed_drafts() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "entry\r\n".into()),
|
||||
("part.arcadia".into(), "雨 draft\r\n".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let reopened = Workspace::new(&bundle, true);
|
||||
let recovered = Workspace::from_recovery(workspace.recovery_snapshot()).unwrap();
|
||||
assert_ne!(workspace.session(), reopened.session());
|
||||
assert_ne!(workspace.session(), recovered.session());
|
||||
assert_eq!(workspace.bundle(), recovered.bundle());
|
||||
let saved = serde_json::to_value(workspace.recovery_snapshot()).unwrap();
|
||||
assert!(saved.get("session").is_none() && saved.get("revision").is_none());
|
||||
|
||||
let cursor = Cursor {
|
||||
position: iced::widget::text_editor::Position { line: 0, column: 3 },
|
||||
selection: None,
|
||||
};
|
||||
let revision = workspace.revision();
|
||||
workspace.edit(workspace.text().into(), cursor);
|
||||
assert_eq!(
|
||||
workspace.revision(),
|
||||
revision,
|
||||
"selection is not a text change"
|
||||
);
|
||||
workspace.switch("part.arcadia", cursor);
|
||||
assert_eq!(
|
||||
workspace.revision(),
|
||||
revision,
|
||||
"switching retains the snapshot"
|
||||
);
|
||||
let untouched = workspace.files["project.arcadia"].generation();
|
||||
workspace.edit("雨 unfinished !\r\n".into(), cursor);
|
||||
let edited = workspace.files["part.arcadia"].generation();
|
||||
assert!(workspace.revision() > revision);
|
||||
assert_eq!(workspace.files["project.arcadia"].generation(), untouched);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
assert!(workspace.files["part.arcadia"].generation() > edited);
|
||||
assert!(workspace.undo(true));
|
||||
assert_eq!(workspace.text(), "雨 unfinished !\r\n");
|
||||
let previous_session = workspace.session();
|
||||
let previous_text = workspace.bundle();
|
||||
workspace.reset_navigation_session();
|
||||
assert_ne!(workspace.session(), previous_session);
|
||||
assert_eq!(workspace.bundle(), previous_text);
|
||||
assert!(
|
||||
workspace.undo(false),
|
||||
"session reset preserves text history"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_scope_changes_invalidate_without_changing_unchanged_positions() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "entry".into()),
|
||||
("part.arcadia".into(), "part".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let entry_generation = workspace.files[&bundle.entry].generation();
|
||||
let initial_revision = workspace.revision();
|
||||
workspace.set_reachable(BTreeSet::from([bundle.entry.clone()]));
|
||||
assert!(workspace.revision() > initial_revision);
|
||||
let membership_revision = workspace.revision();
|
||||
workspace.set_reachable(workspace.reachable.clone());
|
||||
assert_eq!(workspace.revision(), membership_revision);
|
||||
assert!(workspace.discard_detached("part.arcadia"));
|
||||
assert!(workspace.revision() > membership_revision);
|
||||
let closed_revision = workspace.revision();
|
||||
workspace.discovered(&bundle, BTreeSet::from([bundle.entry.clone()]));
|
||||
assert!(workspace.revision() > closed_revision);
|
||||
assert_eq!(
|
||||
workspace.files[&bundle.entry].generation(),
|
||||
entry_generation
|
||||
);
|
||||
assert!(workspace.files["part.arcadia"].generation() > closed_revision);
|
||||
let discovered_revision = workspace.revision();
|
||||
workspace.discovered(&bundle, workspace.reachable.clone());
|
||||
assert_eq!(workspace.revision(), discovered_revision);
|
||||
workspace.accept(&bundle);
|
||||
assert!(
|
||||
workspace.revision() > discovered_revision,
|
||||
"validation evidence changed"
|
||||
);
|
||||
assert_eq!(
|
||||
workspace.files[&bundle.entry].generation(),
|
||||
entry_generation
|
||||
);
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_revision_wrap_cannot_reuse_an_old_workspace_key() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), "original".into())]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
workspace.revision = u64::MAX;
|
||||
let before = workspace.session();
|
||||
let cursor = workspace.files[&bundle.entry].cursor;
|
||||
workspace.edit("changed".into(), cursor);
|
||||
assert_ne!(workspace.session(), before);
|
||||
assert_eq!(workspace.revision(), 0);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
}
|
||||
#[test]
|
||||
fn draft_history_budget_covers_inactive_files_without_discarding_drafts() {
|
||||
let bundle = SourceBundle {
|
||||
|
||||
@@ -377,7 +377,8 @@ fn source_navigation_waits_for_editor_layout_and_cancels_on_edit() {
|
||||
send(&mut app, Message::SourceForSelection);
|
||||
assert_eq!(app.page, Page::Text);
|
||||
assert!(app.source_jump.is_some());
|
||||
send(&mut app, Message::SourceViewport);
|
||||
let ticket = app.source_reveal_ticket();
|
||||
send(&mut app, Message::SourceViewport(ticket));
|
||||
assert!(app.source_jump.is_none());
|
||||
send(&mut app, Message::SourceForSelection);
|
||||
assert!(app.source_jump.is_some());
|
||||
@@ -409,7 +410,8 @@ fn source_find_navigates_an_invalid_draft_without_applying_or_losing_it() {
|
||||
cursor.selection,
|
||||
Some(text_editor::Position { line: 0, column: 9 })
|
||||
);
|
||||
send(&mut app, Message::SourceViewport);
|
||||
let ticket = app.source_reveal_ticket();
|
||||
send(&mut app, Message::SourceViewport(ticket));
|
||||
assert_eq!(app.source_editor.cursor(), cursor);
|
||||
send(
|
||||
&mut app,
|
||||
@@ -1269,7 +1271,26 @@ fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner()
|
||||
.get_mut(&ancestor)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert!(crate::navigation::diagrams_containing(&p, port).contains(&diagram));
|
||||
// The usage query reports rendered objects; this port's owner is folded.
|
||||
// The legacy authoring reveal below explicitly expands its saved ancestor.
|
||||
assert!(
|
||||
!oa_core::projection::project_view(&p, diagram)
|
||||
.ports
|
||||
.iter()
|
||||
.any(|shown| shown.element == port)
|
||||
);
|
||||
assert!(crate::diagram_usage::saved_usages_for(&p, port, Some(diagram)).is_empty());
|
||||
// Other saved views of this function may still paint the port.
|
||||
let elsewhere = crate::diagram_usage::saved_usages(&p, port);
|
||||
assert!(
|
||||
!elsewhere.is_empty(),
|
||||
"the demo still paints this port in another saved view"
|
||||
);
|
||||
assert!(
|
||||
elsewhere
|
||||
.iter()
|
||||
.all(|usage| usage.surface != crate::diagram_usage::Surface::Saved(diagram))
|
||||
);
|
||||
let mut app = App::with_project(p);
|
||||
send(&mut app, Message::RevealInDiagram(diagram, port));
|
||||
assert_eq!(app.selected, port);
|
||||
@@ -1281,6 +1302,12 @@ fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner()
|
||||
.iter()
|
||||
.any(|p| p.element == port)
|
||||
);
|
||||
assert!(
|
||||
crate::diagram_usage::saved_usages_for(app.project(), port, Some(diagram))
|
||||
.iter()
|
||||
.any(|usage| matches!(usage.target,
|
||||
crate::diagram_usage::Target::Port { port: found, .. } if found == port))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+508
-10
@@ -1,3 +1,4 @@
|
||||
mod source_symbols;
|
||||
pub(crate) mod traceability;
|
||||
use crate::{
|
||||
app::{App, Choice, FileOperation, Message, Page, Pending},
|
||||
@@ -449,6 +450,9 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
view_index: Some(app.document.view_index()),
|
||||
diagram: id,
|
||||
selected: app.selected,
|
||||
navigation_focus: app
|
||||
.diagram_focus(crate::diagram_usage::Surface::Saved(id))
|
||||
.map(|usage| usage.bounds),
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
@@ -469,10 +473,17 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
if let Some(editor) = crate::app::architecture_authoring::inline_editor(app) {
|
||||
scene = scene.push(editor);
|
||||
}
|
||||
let reveal = app.architecture_reveal_stamp(id);
|
||||
row![
|
||||
widget::sensor(scene)
|
||||
.on_show(Message::CanvasResized)
|
||||
.on_resize(Message::CanvasResized),
|
||||
.on_show(move |size| reveal
|
||||
.map_or(Message::CanvasResized(size), |(context, epoch)| {
|
||||
Message::DiagramRevealViewport(context, epoch, size)
|
||||
}))
|
||||
.on_resize(move |size| reveal
|
||||
.map_or(Message::CanvasResized(size), |(context, epoch)| {
|
||||
Message::DiagramRevealViewport(context, epoch, size)
|
||||
})),
|
||||
crate::app::architecture_authoring::palette(app)
|
||||
]
|
||||
.height(Fill)
|
||||
@@ -588,7 +599,7 @@ fn inspector(app: &App) -> View<'_, Message> {
|
||||
return container(
|
||||
column![
|
||||
tabs,
|
||||
scrollable(crate::navigation::view(p, app.selected)).height(Fill)
|
||||
scrollable(crate::navigation::view(app, app.selected)).height(Fill)
|
||||
]
|
||||
.spacing(14),
|
||||
)
|
||||
@@ -1022,7 +1033,7 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
),
|
||||
action("Next", Message::SourceNavigation(SourceEvent::Next(false))),
|
||||
label(app.source_navigation.label()),
|
||||
action("Close", Message::SourceNavigation(SourceEvent::Close)),
|
||||
action("Close", app.source_find_close_message()),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
@@ -1116,8 +1127,12 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
}
|
||||
let editor = widget::text_editor(&app.source_editor)
|
||||
.id("source-editor")
|
||||
.id(app.source_editor_id())
|
||||
.on_action(Message::SourceEdit)
|
||||
.key_binding({
|
||||
let context = app.source_symbol_context();
|
||||
move |press| source_binding(press, &context)
|
||||
})
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
@@ -1126,8 +1141,7 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
app.source_snapshot.clone(),
|
||||
crate::highlight::format,
|
||||
);
|
||||
column![
|
||||
container(
|
||||
let header = container(
|
||||
column![
|
||||
row![
|
||||
container(heading(
|
||||
@@ -1170,17 +1184,501 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
"Undo text affects this file's draft. Model Undo restores accepted files together (100 changes / about 32 MiB)."
|
||||
}),
|
||||
navigation,
|
||||
row![
|
||||
action("Definition · F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::Definition))),
|
||||
action("References · Shift F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::References))),
|
||||
action("Show in diagrams…", app.source_symbol_message(crate::source_navigation::SymbolEvent::DiagramUsages)),
|
||||
button(text("Back · Alt ←").size(12)).padding([8, 10]).style(style::nav)
|
||||
.on_press_maybe((app.source_symbols.back.iter().any(|entry| entry.session == app.source_workspace.session())).then(|| app.source_symbol_message(crate::source_navigation::SymbolEvent::Back))),
|
||||
].spacing(7),
|
||||
scrollable(diagnostics)
|
||||
.height((visible_diagnostics as f32 * 32.).min(132.))
|
||||
]
|
||||
.spacing(12)
|
||||
)
|
||||
.padding(18),
|
||||
widget::sensor(editor).key((app.source_workspace.active.clone(),app.source_jump))
|
||||
.on_show(|_| Message::SourceViewport).on_resize(|_| Message::SourceViewport)
|
||||
.padding(18);
|
||||
let editor = widget::sensor(editor)
|
||||
.key((
|
||||
app.source_workspace.active.clone(),
|
||||
app.source_reveal_ticket(),
|
||||
))
|
||||
.on_show({
|
||||
let ticket = app.source_reveal_ticket();
|
||||
move |_| Message::SourceViewport(ticket)
|
||||
})
|
||||
.on_resize({
|
||||
let ticket = app.source_reveal_ticket();
|
||||
move |_| Message::SourceViewport(ticket)
|
||||
});
|
||||
let workspace: View<'_, Message> = if app.source_symbols.open {
|
||||
row![
|
||||
container(editor).width(Fill).height(Fill),
|
||||
container(scrollable(source_symbols::panel(app)))
|
||||
.width(300)
|
||||
.height(Fill),
|
||||
]
|
||||
.height(Fill)
|
||||
.into()
|
||||
} else {
|
||||
container(editor).width(Fill).height(Fill).into()
|
||||
};
|
||||
source_layout(header.into(), workspace, app.window_size.height)
|
||||
}
|
||||
|
||||
fn source_layout<'a, Renderer: iced::advanced::text::Renderer + 'a>(
|
||||
header: View<'a, Message, iced::Theme, Renderer>,
|
||||
workspace: View<'a, Message, iced::Theme, Renderer>,
|
||||
window_height: f32,
|
||||
) -> View<'a, Message, iced::Theme, Renderer> {
|
||||
// Lint clears and repopulates diagnostics asynchronously. Reserve the same
|
||||
// outer slot so a pointer aimed at the editor cannot land in the header.
|
||||
column![
|
||||
container(scrollable(header).id("source-header")).height((window_height * 0.42).max(180.)),
|
||||
workspace,
|
||||
]
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
fn source_binding(
|
||||
press: widget::text_editor::KeyPress,
|
||||
context: &crate::source_navigation::SymbolContext,
|
||||
) -> Option<widget::text_editor::Binding<Message>> {
|
||||
use crate::source_navigation::{SymbolEvent, query::Action};
|
||||
use iced::keyboard::{Key, key::Named};
|
||||
use widget::text_editor::{Binding, Status};
|
||||
// Windows owns Alt+Space for its window menu. Iced's default binding would
|
||||
// otherwise insert a supplied space payload, even after that menu closes.
|
||||
// Keep Ctrl+Alt (AltGr), shifted input, and ordinary Space unchanged.
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& press.modifiers == iced::keyboard::Modifiers::ALT
|
||||
&& matches!(
|
||||
press.key.as_ref(),
|
||||
Key::Named(Named::Space) | Key::Character(" ")
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& !press.modifiers.control()
|
||||
&& !press.modifiers.logo()
|
||||
{
|
||||
let event = match &press.key {
|
||||
Key::Named(Named::F12) if !press.modifiers.alt() => {
|
||||
Some(SymbolEvent::Query(if press.modifiers.shift() {
|
||||
Action::References
|
||||
} else {
|
||||
Action::Definition
|
||||
}))
|
||||
}
|
||||
Key::Named(Named::ArrowLeft) if press.modifiers.alt() && !press.modifiers.shift() => {
|
||||
Some(SymbolEvent::Back)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(event) = event {
|
||||
return Some(Binding::Custom(Message::SourceSymbols(
|
||||
context.clone(),
|
||||
event,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Binding::from_key_press(press)
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod source_tests {
|
||||
use super::*;
|
||||
use iced::advanced::{
|
||||
Layout, Shell, graphics, layout, renderer, text as shaped, widget as core_widget,
|
||||
};
|
||||
use iced::widget::column;
|
||||
use iced::{Background, Color, Pixels, Point, Rectangle, Size, Transformation, Vector};
|
||||
|
||||
// Measure real Iced paragraphs/editor text without constructing a window or GPU.
|
||||
struct FontLayout;
|
||||
impl renderer::Renderer for FontLayout {
|
||||
fn start_layer(&mut self, _: Rectangle) {}
|
||||
fn end_layer(&mut self) {}
|
||||
fn start_transformation(&mut self, _: Transformation) {}
|
||||
fn end_transformation(&mut self) {}
|
||||
fn reset(&mut self, _: Rectangle) {}
|
||||
fn fill_quad(&mut self, _: renderer::Quad, _: impl Into<Background>) {}
|
||||
fn allocate_image(
|
||||
&mut self,
|
||||
handle: &iced::advanced::image::Handle,
|
||||
callback: impl FnOnce(
|
||||
Result<iced::advanced::image::Allocation, iced::advanced::image::Error>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
renderer::Renderer::allocate_image(&mut (), handle, callback);
|
||||
}
|
||||
}
|
||||
impl shaped::Renderer for FontLayout {
|
||||
type Font = Font;
|
||||
type Paragraph = graphics::text::Paragraph;
|
||||
type Editor = graphics::text::Editor;
|
||||
const ICON_FONT: Font = Font::DEFAULT;
|
||||
const CHECKMARK_ICON: char = '0';
|
||||
const ARROW_DOWN_ICON: char = '0';
|
||||
const SCROLL_UP_ICON: char = '0';
|
||||
const SCROLL_DOWN_ICON: char = '0';
|
||||
const SCROLL_LEFT_ICON: char = '0';
|
||||
const SCROLL_RIGHT_ICON: char = '0';
|
||||
const ICED_LOGO: char = '0';
|
||||
fn default_font(&self) -> Font {
|
||||
Font::DEFAULT
|
||||
}
|
||||
fn default_size(&self) -> Pixels {
|
||||
Pixels(16.)
|
||||
}
|
||||
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
|
||||
fn fill_editor(&mut self, _: &Self::Editor, _: Point, _: Color, _: Rectangle) {}
|
||||
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Geometry {
|
||||
header: Option<(Rectangle, Rectangle, Vector)>,
|
||||
editor: Option<Rectangle>,
|
||||
}
|
||||
impl core_widget::Operation for Geometry {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn core_widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn container(&mut self, id: Option<&core_widget::Id>, bounds: Rectangle) {
|
||||
if id == Some(&core_widget::Id::new("test-source-editor")) {
|
||||
self.editor = Some(bounds);
|
||||
}
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&core_widget::Id>,
|
||||
bounds: Rectangle,
|
||||
content: Rectangle,
|
||||
offset: Vector,
|
||||
_: &mut dyn core_widget::operation::Scrollable,
|
||||
) {
|
||||
if id == Some(&core_widget::Id::new("source-header")) {
|
||||
self.header = Some((bounds, content, offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mounted_source<'a>(
|
||||
content: &'a widget::text_editor::Content<FontLayout>,
|
||||
diagnostics: usize,
|
||||
find: bool,
|
||||
window_height: f32,
|
||||
) -> View<'a, Message, iced::Theme, FontLayout> {
|
||||
let mut notices = column![].spacing(5);
|
||||
for index in 0..diagnostics {
|
||||
notices = notices.push(button(text(format!(
|
||||
"救援/assess.arcadia:{}:5 · Warning: verify this participant's engineering allocation", index + 1,
|
||||
)).size(12)).padding([4, 6]).width(Fill));
|
||||
}
|
||||
let find: View<'_, Message, iced::Theme, FontLayout> = if find {
|
||||
column![
|
||||
text_input("Find literal text", "assess").on_input(|s| Message::SourceNavigation(
|
||||
crate::source_navigation::Event::Query(s)
|
||||
)),
|
||||
text_input("Line", "1").on_input(|s| Message::SourceNavigation(
|
||||
crate::source_navigation::Event::Line(s)
|
||||
)),
|
||||
]
|
||||
.spacing(8)
|
||||
.into()
|
||||
} else {
|
||||
Space::new().into()
|
||||
};
|
||||
let header = container(
|
||||
column![
|
||||
text("Textual model").size(24),
|
||||
text("Edit the current file. Apply text validates the complete included model.")
|
||||
.size(11),
|
||||
row![
|
||||
button(text("Apply text")),
|
||||
button(text("Undo text")),
|
||||
button(text("Model Undo"))
|
||||
]
|
||||
.spacing(8),
|
||||
text("救援/assess.arcadia · All files applied to the accepted model").size(11),
|
||||
text("Undo text affects this draft. Model Undo restores accepted files together.")
|
||||
.size(11),
|
||||
find,
|
||||
row![
|
||||
button(text("Definition · F12")),
|
||||
button(text("References · Shift F12")),
|
||||
button(text("Back"))
|
||||
]
|
||||
.spacing(7),
|
||||
scrollable(notices).height((diagnostics as f32 * 32.).min(132.)),
|
||||
]
|
||||
.spacing(12),
|
||||
)
|
||||
.padding(18);
|
||||
let editor = container(
|
||||
widget::text_editor(content)
|
||||
.on_action(Message::SourceEdit)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(Fill),
|
||||
)
|
||||
.id("test-source-editor")
|
||||
.height(Fill)
|
||||
.width(Fill);
|
||||
source_layout(header.into(), editor.into(), window_height)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_header_keeps_editor_bounds_stable_through_lint_and_find_rebuilds() {
|
||||
for (host, window_height) in [
|
||||
(Size::new(720., 540.), 700.),
|
||||
(Size::new(1180., 890.), 1080.),
|
||||
] {
|
||||
let content = widget::text_editor::Content::<FontLayout>::with_text(
|
||||
"// is excluded\r\n// 雨\r\n",
|
||||
);
|
||||
let mut tree = core_widget::Tree::empty();
|
||||
let mut baseline = None;
|
||||
for (diagnostics, find) in [
|
||||
(0, false),
|
||||
(6, false),
|
||||
(0, false),
|
||||
(6, true),
|
||||
(1, true),
|
||||
(0, false),
|
||||
] {
|
||||
let mut ui = mounted_source(&content, diagnostics, find, window_height);
|
||||
tree.diff(ui.as_widget());
|
||||
let node = ui.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, host),
|
||||
);
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut().operate(
|
||||
&mut tree,
|
||||
Layout::new(&node),
|
||||
&FontLayout,
|
||||
&mut geometry,
|
||||
);
|
||||
let editor = geometry.editor.unwrap();
|
||||
let (header, _, _) = geometry.header.unwrap();
|
||||
let slot = node.children()[0].bounds();
|
||||
assert_eq!(slot.height, (window_height * 0.42).max(180.));
|
||||
assert!(
|
||||
header.height <= slot.height,
|
||||
"short content may shrink inside the fixed slot"
|
||||
);
|
||||
assert!(
|
||||
editor.height >= 240.,
|
||||
"remaining editor must stay usable: {editor:?}"
|
||||
);
|
||||
assert_eq!(editor.y, slot.y + slot.height);
|
||||
if let Some(expected) = baseline {
|
||||
assert_eq!(editor, expected, "diagnostics={diagnostics}, find={find}");
|
||||
} else {
|
||||
baseline = Some(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_header_remains_scrollable_and_preserves_offset_across_diagnostic_refresh() {
|
||||
let content = widget::text_editor::Content::<FontLayout>::with_text("// 雨\r\n");
|
||||
let host = Size::new(720., 540.);
|
||||
let mut tree = core_widget::Tree::empty();
|
||||
let mut saved = None;
|
||||
for diagnostics in [6, 7] {
|
||||
let mut ui = mounted_source(&content, diagnostics, true, 700.);
|
||||
tree.diff(ui.as_widget());
|
||||
let node = ui.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, host),
|
||||
);
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut geometry);
|
||||
let (bounds, contents, _) = geometry.header.unwrap();
|
||||
assert!(
|
||||
contents.height > bounds.height,
|
||||
"fixture must actually overflow"
|
||||
);
|
||||
if saved.is_none() {
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
ui.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
|
||||
delta: iced::mouse::ScrollDelta::Pixels { x: 0., y: -45. },
|
||||
}),
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Available(Point::new(bounds.x + 8., bounds.y + 8.)),
|
||||
&FontLayout,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(host),
|
||||
);
|
||||
assert!(shell.is_event_captured());
|
||||
}
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut geometry);
|
||||
let offset = geometry.header.unwrap().2;
|
||||
assert!(offset.y > 0., "the actual mounted Scrollable must move");
|
||||
if let Some(before) = saved {
|
||||
assert_eq!(offset, before);
|
||||
} else {
|
||||
saved = Some(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn source_editor_windows_menu_payload_is_inert_but_space_and_altgr_still_edit() {
|
||||
use core_widget::operation::Focusable;
|
||||
use iced::keyboard::{
|
||||
self, Key, Modifiers,
|
||||
key::{Code, Named, Physical},
|
||||
};
|
||||
use widget::text_editor::{Binding, Content, KeyPress, State, Status};
|
||||
let baseline = "// is excluded\r\n// 雨\r\n";
|
||||
let context = crate::source_navigation::SymbolContext {
|
||||
session: Id::new_v4(),
|
||||
path: "救援/assess.arcadia".into(),
|
||||
};
|
||||
for (key, physical_key, modifiers, payload, inserted) in [
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT,
|
||||
" ",
|
||||
false,
|
||||
),
|
||||
(
|
||||
Key::Character(" ".into()),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT,
|
||||
" ",
|
||||
false,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::empty(),
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::CTRL | Modifiers::ALT,
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Character("e".into()),
|
||||
Physical::Code(Code::KeyE),
|
||||
Modifiers::CTRL | Modifiers::ALT,
|
||||
"€",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT | Modifiers::SHIFT,
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
] {
|
||||
let mut content = Content::<FontLayout>::with_text(baseline);
|
||||
content.move_to(widget::text_editor::Cursor {
|
||||
position: widget::text_editor::Position { line: 0, column: 5 },
|
||||
selection: None,
|
||||
});
|
||||
let press = KeyPress {
|
||||
key: key.clone(),
|
||||
modified_key: key.clone(),
|
||||
physical_key,
|
||||
modifiers,
|
||||
text: Some(payload.into()),
|
||||
status: Status::Focused { is_hovered: false },
|
||||
};
|
||||
assert!(
|
||||
matches!(
|
||||
Binding::<Message>::from_key_press(press),
|
||||
Some(Binding::Insert(_))
|
||||
),
|
||||
"the supplied event must expose the default insertion path"
|
||||
);
|
||||
let mut messages = Vec::new();
|
||||
{
|
||||
let mut editor: View<'_, Message, iced::Theme, FontLayout> =
|
||||
widget::text_editor(&content)
|
||||
.on_action(Message::SourceEdit)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(Fill)
|
||||
.key_binding(|press| source_binding(press, &context))
|
||||
.into();
|
||||
let mut tree = core_widget::Tree::new(editor.as_widget());
|
||||
let size = Size::new(600., 240.);
|
||||
let node = editor.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, size),
|
||||
);
|
||||
tree.state
|
||||
.downcast_mut::<State<shaped::highlighter::PlainText>>()
|
||||
.focus();
|
||||
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: key.clone(),
|
||||
modified_key: key,
|
||||
physical_key,
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers,
|
||||
text: Some(payload.into()),
|
||||
repeat: false,
|
||||
});
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
editor.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&event,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&FontLayout,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(size),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
messages.len(),
|
||||
usize::from(inserted),
|
||||
"modifiers={modifiers:?}"
|
||||
);
|
||||
for message in messages {
|
||||
let Message::SourceEdit(action) = message else {
|
||||
panic!("unexpected navigation action");
|
||||
};
|
||||
content.perform(action);
|
||||
}
|
||||
let expected = if inserted {
|
||||
format!("// is{payload} excluded\r\n// 雨\r\n")
|
||||
} else {
|
||||
baseline.into()
|
||||
};
|
||||
assert_eq!(content.text(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn checks(app: &App) -> View<'_, Message> {
|
||||
let issues = &app.model_issues;
|
||||
let mut out = column![heading(
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
use crate::{
|
||||
app::{App, Message},
|
||||
source_navigation::{
|
||||
SymbolEvent,
|
||||
query::{self, Declaration, Disposition, Evidence, Identity, Membership, Position},
|
||||
},
|
||||
style,
|
||||
};
|
||||
use iced::{
|
||||
Element, Fill,
|
||||
widget::{Space, button, column, container, row, scrollable, text, tooltip},
|
||||
};
|
||||
|
||||
pub(super) fn panel(app: &App) -> Element<'_, Message> {
|
||||
let state = &app.source_symbols;
|
||||
if !state.open
|
||||
|| (state.results.is_none() && state.in_flight.is_none() && state.notice.is_none())
|
||||
{
|
||||
return column![].into();
|
||||
}
|
||||
let title = state.results.as_ref().map_or_else(
|
||||
|| "Source symbols".to_owned(),
|
||||
|results| {
|
||||
let action = match results.action {
|
||||
query::Action::Definition => "Definitions",
|
||||
query::Action::References => "References",
|
||||
};
|
||||
results.alias.as_ref().map_or_else(
|
||||
|| action.to_owned(),
|
||||
|alias| format!("{action} · @{}", preview(alias, 40)),
|
||||
)
|
||||
},
|
||||
);
|
||||
let mut content = column![
|
||||
row![
|
||||
text(title).size(13),
|
||||
Space::new().width(Fill),
|
||||
button(text("Close").size(11))
|
||||
.padding([3, 8])
|
||||
.style(style::nav)
|
||||
.on_press(app.source_symbol_message(SymbolEvent::Close)),
|
||||
]
|
||||
.align_y(iced::Center)
|
||||
]
|
||||
.spacing(5);
|
||||
|
||||
if let Some(notice) = &state.notice {
|
||||
content = content.push(super::label(preview(notice, 240)));
|
||||
}
|
||||
if state.in_flight.is_some() {
|
||||
content = content.push(super::label("Finding source symbols…"));
|
||||
} else if let Some(results) = &state.results {
|
||||
let stale = results.stamp.snapshot != app.source_key();
|
||||
if stale {
|
||||
content = content.push(super::label(
|
||||
"Stale results — source changed. Run Definition or References again.",
|
||||
));
|
||||
}
|
||||
let scope = &results.scope;
|
||||
content = content.push(super::label(format!(
|
||||
"{} · {} · {} included / {} detached / {} unverified files · {} scope issues",
|
||||
if scope.partial() {
|
||||
"Partial scope"
|
||||
} else {
|
||||
"Complete include scope"
|
||||
},
|
||||
if !scope.applied {
|
||||
"Unapplied drafts"
|
||||
} else if scope.validated {
|
||||
"Validated sources"
|
||||
} else {
|
||||
"Draft sources"
|
||||
},
|
||||
scope.included_files,
|
||||
scope.detached_files,
|
||||
scope.unverified_files,
|
||||
scope.issues.len(),
|
||||
)));
|
||||
let explanation = match results.disposition {
|
||||
Disposition::NoSymbol => "Place the caret on a declaration alias or an @reference.",
|
||||
Disposition::NoDefinition => "No matching declaration in the loaded sources.",
|
||||
Disposition::Choices => {
|
||||
"Choose a declaration; draft and detached candidates need an explicit choice."
|
||||
}
|
||||
Disposition::Direct => "Definition in the current sources.",
|
||||
Disposition::LexicalReferences => {
|
||||
"Lexical uses of this alias; these do not establish a semantic target."
|
||||
}
|
||||
};
|
||||
content = content.push(super::label(explanation));
|
||||
if results.total_definitions > 1 {
|
||||
content = content.push(super::label(format!(
|
||||
"Alias collision: {} declarations. Uses are not attributed to a single declaration.",
|
||||
results.total_definitions,
|
||||
)));
|
||||
}
|
||||
let enabled = !stale && !app.busy;
|
||||
let mut rows = column![super::label(counts(
|
||||
"Declarations",
|
||||
results.definitions.len(),
|
||||
results.total_definitions,
|
||||
results.omitted_definitions,
|
||||
))]
|
||||
.spacing(4);
|
||||
for (index, definition) in results.definitions.iter().enumerate() {
|
||||
rows = rows.push(location_row(
|
||||
declaration_title(&definition.declaration),
|
||||
&definition.path,
|
||||
definition.position,
|
||||
provenance(
|
||||
definition.membership,
|
||||
definition.evidence,
|
||||
definition.declaration.identity,
|
||||
false,
|
||||
),
|
||||
enabled.then(|| {
|
||||
app.source_symbol_message(SymbolEvent::Choose {
|
||||
stamp: results.stamp,
|
||||
reference: false,
|
||||
index,
|
||||
})
|
||||
}),
|
||||
));
|
||||
}
|
||||
rows = rows.push(super::label(counts(
|
||||
"Lexical uses",
|
||||
results.references.len(),
|
||||
results.total_references,
|
||||
results.omitted_references,
|
||||
)));
|
||||
for (index, reference) in results.references.iter().enumerate() {
|
||||
let title = reference.context.as_ref().map_or_else(
|
||||
|| format!("@{} · lexical use", preview(&reference.alias, 48)),
|
||||
|context| {
|
||||
format!(
|
||||
"@{} · inside {}",
|
||||
preview(&reference.alias, 48),
|
||||
declaration_title(context)
|
||||
)
|
||||
},
|
||||
);
|
||||
rows = rows.push(location_row(
|
||||
title,
|
||||
&reference.path,
|
||||
reference.position,
|
||||
provenance(
|
||||
reference.membership,
|
||||
reference.evidence,
|
||||
reference
|
||||
.context
|
||||
.as_ref()
|
||||
.map_or(Identity::Missing, |context| context.identity),
|
||||
true,
|
||||
),
|
||||
enabled.then(|| {
|
||||
app.source_symbol_message(SymbolEvent::Choose {
|
||||
stamp: results.stamp,
|
||||
reference: true,
|
||||
index,
|
||||
})
|
||||
}),
|
||||
));
|
||||
}
|
||||
let row_count = results.definitions.len() + results.references.len();
|
||||
content =
|
||||
content.push(scrollable(rows).height((44.0 + row_count as f32 * 48.0).min(180.0)));
|
||||
}
|
||||
container(content)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
.style(style::subtle)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn counts(label: &str, shown: usize, total: usize, omitted: usize) -> String {
|
||||
format!("{label}: {total} total · {shown} shown · {omitted} omitted")
|
||||
}
|
||||
|
||||
fn declaration_title(declaration: &Declaration) -> String {
|
||||
let name = declaration
|
||||
.name
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |name| format!(" \"{}\"", preview(name, 48)));
|
||||
format!(
|
||||
"{}{} · @{}",
|
||||
preview(&declaration.kind, 24),
|
||||
name,
|
||||
preview(&declaration.alias, 48)
|
||||
)
|
||||
}
|
||||
|
||||
fn provenance(
|
||||
membership: Membership,
|
||||
evidence: Evidence,
|
||||
identity: Identity,
|
||||
context: bool,
|
||||
) -> String {
|
||||
let membership = match membership {
|
||||
Membership::Included => "Included",
|
||||
Membership::Detached => "Detached",
|
||||
Membership::Unverified => "Unverified",
|
||||
};
|
||||
let evidence = match evidence {
|
||||
Evidence::Validated => "Validated",
|
||||
Evidence::Draft => "Draft",
|
||||
};
|
||||
let collision = match identity {
|
||||
Identity::Collision { count } => format!(
|
||||
" · {}ID collision ({count})",
|
||||
if context { "Context " } else { "" },
|
||||
),
|
||||
Identity::Missing | Identity::Unique(_) => String::new(),
|
||||
};
|
||||
format!("{membership} · {evidence}{collision}")
|
||||
}
|
||||
|
||||
fn location_row(
|
||||
title: String,
|
||||
path: &str,
|
||||
position: Position,
|
||||
status: String,
|
||||
event: Option<Message>,
|
||||
) -> Element<'static, Message> {
|
||||
let line = position.line.saturating_add(1);
|
||||
let column = position.byte_column.saturating_add(1);
|
||||
let location = format!("{}:{line}:{column}", preview(path, 72));
|
||||
tooltip(
|
||||
button(
|
||||
column![
|
||||
text(title).size(12),
|
||||
super::label(format!("{location} · {status}"))
|
||||
]
|
||||
.spacing(2),
|
||||
)
|
||||
.padding([4, 6])
|
||||
.width(Fill)
|
||||
.style(style::nav)
|
||||
.on_press_maybe(event),
|
||||
text(format!(
|
||||
"{path}:{line}:{column} (1-based line and UTF-8 byte column)"
|
||||
))
|
||||
.size(11),
|
||||
tooltip::Position::Top,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Keep headings compact without copying the remainder of a long admitted value.
|
||||
fn preview(value: &str, limit: usize) -> String {
|
||||
let mut chars = value.chars();
|
||||
let mut result: String = chars
|
||||
.by_ref()
|
||||
.take(limit)
|
||||
.map(|ch| if ch.is_whitespace() { ' ' } else { ch })
|
||||
.collect();
|
||||
if chars.next().is_some() {
|
||||
result.push('…');
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
Diagram representation navigation
|
||||
=================================
|
||||
|
||||
This contract derives from user issues `14: Text symbol usages
|
||||
<http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/14>`_
|
||||
and `17: tree-to-diagram navigation
|
||||
<http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/17>`_.
|
||||
Implementation and acceptance status belong to those issues. The source language
|
||||
and identity rules remain in ``language.rst``.
|
||||
|
||||
Identity and membership
|
||||
-----------------------
|
||||
|
||||
A result identifies the queried subject, a diagram destination, and a specific
|
||||
graphical representation. A destination is a saved diagram UUID, a process or
|
||||
functional-chain UUID, or a scenario UUID. Saved state diagrams retain their own
|
||||
diagram UUID; membership in the same state machine does not make their contents
|
||||
interchangeable.
|
||||
|
||||
Representation identities are node placement, port plus owner placement, edge,
|
||||
state row plus owner placement, process occurrence plus qualified reference path,
|
||||
process link, or scenario lifeline/occurrence. Opening an owner itself is a
|
||||
separate whole-diagram target. Names and aliases describe these identities; they
|
||||
are never replacement identities. Repeated occurrences remain independently
|
||||
selectable even when they represent the same activity or exchange.
|
||||
|
||||
The current renderer determines membership:
|
||||
|
||||
* Saved node, port, and edge results come from the final projection, including
|
||||
folds, projection-kind rules, and view filters. A stored route or two placed
|
||||
endpoints does not establish a visible edge.
|
||||
* Process results use the displayed contribution graph. A contextual preview
|
||||
retains its full caller path and is included only while its actual tile is
|
||||
present. An inactive process uses the same initial preview policy as entering
|
||||
its canvas. Preview adjacency has no invented relation UUID.
|
||||
* Scenario results use the actual scene, preserving lifeline and message
|
||||
occurrence identities, nested fragment operands, and repeated messages.
|
||||
A scenario-reference box represents its called scenario. It does not also
|
||||
represent the called scenario's messages or lifelines.
|
||||
* State action and internal-transition rows retain their relation identity.
|
||||
Visible named references, such as a state action's work or a transition's
|
||||
trigger, can target the row or edge whose label displays that reference.
|
||||
Label-only membership must account for the renderer's shortening policy.
|
||||
|
||||
An arbitrary semantic reference is insufficient. A description mentioning a
|
||||
name, an exchange item hidden by the selected label mode, or an unexpanded callee
|
||||
does not automatically add a diagram result. The result count describes
|
||||
currently rendered representations, not all semantic dependencies. Folded,
|
||||
filtered, and unexpanded representations are explicitly excluded by this version;
|
||||
an empty result must disclose this scope and must not claim that an object is
|
||||
unused everywhere. Semantic dependencies remain available through Connections.
|
||||
|
||||
Two additional representation forms remain gaps in this branch: allocation
|
||||
relations expressed through nesting, and persisted sequence links drawn inside
|
||||
qualified caller previews. These must either receive precise grouping/qualified
|
||||
edge targets or remain explicitly disclosed limitations; a passing direct-edge
|
||||
test does not establish their coverage.
|
||||
|
||||
Interaction and lifecycle
|
||||
-------------------------
|
||||
|
||||
``Show in diagrams`` opens one searchable destination list from the Text caret,
|
||||
an object's context/Actions menu, or Connections. It identifies the subject by
|
||||
name and alias, groups targets by destination, reports counts, and presents a
|
||||
bounded page. A destination then exposes its individual representations. Search
|
||||
matches destination names, aliases, categories, representation names, and roles.
|
||||
Long descriptions remain available without replacing stable identities.
|
||||
|
||||
The Text entry point uses the exact current caret and the source-symbol service's
|
||||
immutable snapshot. It requires a unique, validated, included declaration whose
|
||||
authored identity and alias still match the accepted model. It never substitutes
|
||||
the previously selected tree object. Unapplied source is retained with an explicit
|
||||
instruction to apply it before diagram lookup. Comment, prose, ambiguous, missing,
|
||||
detached, and stale symbols cannot trigger a diagram jump.
|
||||
|
||||
Every open request captures the document instance, accepted revision, and subject.
|
||||
Every chooser action captures that chooser's unique token. Before opening a
|
||||
representation, re-resolve the full subject-to-target relationship and renderer
|
||||
geometry. If the model or presentation changed, refresh the list and require a
|
||||
fresh choice. Matching names, reused aliases, or surviving endpoints cannot make
|
||||
a stale target current. An asynchronous source response must also match its job,
|
||||
source session/revision, raw interaction, active file, and caret.
|
||||
|
||||
Lookup and reveal do not execute model commands, Apply, Revert, or Save. They do
|
||||
not expand persisted folds, change filters, rewrite source, or alter undo/redo.
|
||||
Pending edits that would be displaced prevent the jump and remain intact.
|
||||
Successful reveal updates only the navigation owner/page, precise selection,
|
||||
tool lifetime, and camera. The camera centers the target at a readable scale;
|
||||
fitting a huge whole diagram is not sufficient evidence of locating one object.
|
||||
Escape closes the chooser and restores Text focus/caret when invoked there.
|
||||
|
||||
Verification obligations
|
||||
------------------------
|
||||
|
||||
Independent fixtures must cover actual versus filtered edges, folded children,
|
||||
ports, same-name distinct identities, repeated work/message/lifeline occurrences,
|
||||
qualified reuse paths, opaque scenario references, state rows and label references.
|
||||
For each activation, compare the complete accepted Project, SourceBundle, raw
|
||||
buffers, revision, dirty flags, and undo/redo reachability before and after.
|
||||
Exercise stale document/revision/token/subject relationships and retained drafts.
|
||||
|
||||
Native acceptance additionally checks keyboard and pointer selection, exact
|
||||
visible target highlight, camera behavior after the destination's first layout,
|
||||
empty and multi-result states, readable long Unicode names, minimum-window layout,
|
||||
return to editing, and representative large-model latency. Unit tests and a
|
||||
successful build do not by themselves establish these interaction outcomes.
|
||||
@@ -2642,6 +2642,46 @@ trace links, occurrence identity, comments and file ownership. Each open file
|
||||
retains its own unapplied draft across file navigation. Located lint diagnostics
|
||||
refer to the raw draft bytes rather than an identity-stabilized intermediate.
|
||||
Property editing MUST NOT silently discard a pending source draft.</p>
|
||||
<p>In Textual model, place the caret on a declaration alias or an <span class="docutils literal">@reference</span>
|
||||
and choose <strong>Definition</strong> (<span class="docutils literal">F12</span>) or <strong>References</strong> (<span class="docutils literal">Shift+F12</span>).
|
||||
<strong>Back</strong> (<span class="docutils literal">Alt+Left</span>) returns to the previous source location in this
|
||||
workspace. These shortcuts act while the source editor has focus; the Text
|
||||
toolbar also exposes the actions. If the previous draft changed, Back restores
|
||||
its current cursor rather than reusing an obsolete selection.</p>
|
||||
<p>Symbol navigation reads bounded snapshots of the currently loaded source
|
||||
drafts. It does not apply drafts or load missing included files. The results
|
||||
identify partial scope, draft evidence, detached or unverified files, alias
|
||||
ambiguity and authored-ID collisions. Draft, detached and ambiguous definition
|
||||
candidates require an explicit row choice. References are lexical uses of an
|
||||
alias, not a list of resolved typed relationships; comments and strings are
|
||||
excluded. Declaration and use counts include any rows omitted by the result
|
||||
limits. If sources change, stale rows cannot navigate: run the query again.
|
||||
Navigation MUST preserve the accepted model and every source draft, and select
|
||||
locations in the original UTF-8 bytes. Displayed locations use one-based lines
|
||||
and UTF-8 byte columns.</p>
|
||||
<p><strong>Show in diagrams</strong> finds representations of a model record in saved diagrams,
|
||||
process/chain canvases and scenarios. Use the Text toolbar with the caret on a
|
||||
unique, verified alias in clean accepted sources, or the model tree's context
|
||||
menu for the chosen record. Choose a destination, then its exact representation.
|
||||
Lookup runs from an accepted-model snapshot; navigation preserves source drafts,
|
||||
accepted geometry and history. Pending edits or modal operations must be
|
||||
finished first. Changed or stale results cannot activate a different target.</p>
|
||||
<p>Membership follows the actual projection, including visible ports, state action
|
||||
and internal-transition rows, qualified reuse tiles, and SequenceLink arrows
|
||||
painted inside caller previews. Repeated callers have distinct destinations
|
||||
within the same canvas. Hidden preview endpoints and deeper nested links that
|
||||
the renderer does not draw are excluded; projected adjacency without an actual
|
||||
relation is not a SequenceLink usage. Preview-arrow focus lies on a visible
|
||||
line segment outside opaque tiles.</p>
|
||||
<p>An Allocation projection represents a direct Allocation relation by grouping
|
||||
only when its visible source performer placement occurs on the visible target
|
||||
work placement's actual visual-parent chain. Aggregate-only allocation summaries
|
||||
do not create direct relation usages. The navigation marker can focus a work
|
||||
header, port owner or state row independently of semantic selection: selecting
|
||||
an Allocation or state-action relation retains that relation as the model action
|
||||
target. Camera movement preserves a valid marker; changing the selected record,
|
||||
document, accepted revision or represented context retires it. Navigation does
|
||||
not alter allocation ownership or the meaning of Delete.</p>
|
||||
<p>Changing a graphical UUID must atomically update map keys and every reference.
|
||||
Alias changes preserve UUID identity. Deletion must account for owned elements,
|
||||
incident relationships, ordered occurrences, and diagram placements without
|
||||
|
||||
@@ -2064,6 +2064,51 @@ retains its own unapplied draft across file navigation. Located lint diagnostics
|
||||
refer to the raw draft bytes rather than an identity-stabilized intermediate.
|
||||
Property editing MUST NOT silently discard a pending source draft.
|
||||
|
||||
In Textual model, place the caret on a declaration alias or an ``@reference``
|
||||
and choose **Definition** (``F12``) or **References** (``Shift+F12``).
|
||||
**Back** (``Alt+Left``) returns to the previous source location in this
|
||||
workspace. These shortcuts act while the source editor has focus; the Text
|
||||
toolbar also exposes the actions. If the previous draft changed, Back restores
|
||||
its current cursor rather than reusing an obsolete selection.
|
||||
|
||||
Symbol navigation reads bounded snapshots of the currently loaded source
|
||||
drafts. It does not apply drafts or load missing included files. The results
|
||||
identify partial scope, draft evidence, detached or unverified files, alias
|
||||
ambiguity and authored-ID collisions. Draft, detached and ambiguous definition
|
||||
candidates require an explicit row choice. References are lexical uses of an
|
||||
alias, not a list of resolved typed relationships; comments and strings are
|
||||
excluded. Declaration and use counts include any rows omitted by the result
|
||||
limits. If sources change, stale rows cannot navigate: run the query again.
|
||||
Navigation MUST preserve the accepted model and every source draft, and select
|
||||
locations in the original UTF-8 bytes. Displayed locations use one-based lines
|
||||
and UTF-8 byte columns.
|
||||
|
||||
**Show in diagrams** finds representations of a model record in saved diagrams,
|
||||
process/chain canvases and scenarios. Use the Text toolbar with the caret on a
|
||||
unique, verified alias in clean accepted sources, or the model tree's context
|
||||
menu for the chosen record. Choose a destination, then its exact representation.
|
||||
Lookup runs from an accepted-model snapshot; navigation preserves source drafts,
|
||||
accepted geometry and history. Pending edits or modal operations must be
|
||||
finished first. Changed or stale results cannot activate a different target.
|
||||
|
||||
Membership follows the actual projection, including visible ports, state action
|
||||
and internal-transition rows, qualified reuse tiles, and SequenceLink arrows
|
||||
painted inside caller previews. Repeated callers have distinct destinations
|
||||
within the same canvas. Hidden preview endpoints and deeper nested links that
|
||||
the renderer does not draw are excluded; projected adjacency without an actual
|
||||
relation is not a SequenceLink usage. Preview-arrow focus lies on a visible
|
||||
line segment outside opaque tiles.
|
||||
|
||||
An Allocation projection represents a direct Allocation relation by grouping
|
||||
only when its visible source performer placement occurs on the visible target
|
||||
work placement's actual visual-parent chain. Aggregate-only allocation summaries
|
||||
do not create direct relation usages. The navigation marker can focus a work
|
||||
header, port owner or state row independently of semantic selection: selecting
|
||||
an Allocation or state-action relation retains that relation as the model action
|
||||
target. Camera movement preserves a valid marker; changing the selected record,
|
||||
document, accepted revision or represented context retires it. Navigation does
|
||||
not alter allocation ownership or the meaning of Delete.
|
||||
|
||||
Changing a graphical UUID must atomically update map keys and every reference.
|
||||
Alias changes preserve UUID identity. Deletion must account for owned elements,
|
||||
incident relationships, ordered occurrences, and diagram placements without
|
||||
|
||||
@@ -346,3 +346,80 @@ regressions; it does not broaden admission or add a new rule. Compilation and
|
||||
native acceptance of the renderer are still pending at this decision. Revisit the
|
||||
visual encoding if actual minimum-size engineering workflows make marker identity,
|
||||
uncertain continuation or the distinction between guide and existence unclear.
|
||||
|
||||
## Diagram representation navigation — 2026-09-12
|
||||
|
||||
Codex-ytr83r /root. User issues [14](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/14)
|
||||
and [17](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/17)
|
||||
share one typed query and destination chooser; the [contract](../docs/diagram-navigation.rst)
|
||||
defines identity, visible membership, source freshness and nonmutation. A result
|
||||
retains its representation identity separately from its queried semantic subject.
|
||||
Use the actual saved projection, process Graph, scenario Scene and state row
|
||||
geometry. Repeated occurrences and qualified reuse paths remain distinct.
|
||||
|
||||
Reject endpoint-only membership and generic semantic-reference enumeration:
|
||||
they admit suppressed edges and opaque callee contents while missing behavioral
|
||||
occurrences and state rows. Reject a destination submenu containing every use or
|
||||
always-visible per-line lenses: both add noise and scale poorly for the user's
|
||||
editing workflow. An explicitly invoked, searchable chooser groups destinations
|
||||
and exposes their occurrences on demand. Independent design work identified
|
||||
minimum-window, repeated-name, source-caret, stale-result and full-history oracles
|
||||
before implementation; passing those evaluators remains necessary for acceptance.
|
||||
|
||||
Ordinary navigation must not reuse the legacy reveal action's implicit Apply and
|
||||
persisted fold expansion. Resolve the exact target before changing presentation;
|
||||
center that target at readable scale. This first implementation lists currently
|
||||
rendered representations and explicitly discloses folded/filtered/unexpanded
|
||||
exclusions. It does not claim complete authored-membership discovery. Revisit
|
||||
transient hidden-object reveal after measured user workflows; adding a result
|
||||
must never imply an invisible object was actually shown or silently edit a view.
|
||||
|
||||
The Text entry point builds on delivered source navigation from33c6d83, including
|
||||
eeeaa12 native corrections, while preserving the later canvas/chooser/repair
|
||||
integration. It requires verified applied-source identity and never substitutes
|
||||
model selection for the caret. Dirty-source semantic lookup remains a disclosed
|
||||
limitation. Implementation is branch-specific at this decision; Windows build,
|
||||
combined tests and native acceptance are not established by this record.
|
||||
|
||||
|
||||
## 2026-09-12 — Cross-machine coordination evidence
|
||||
|
||||
User clarification: independent Gitea peer sessions run on different machines and cannot access this session’s `.local` directory. In-thread delegated agents share this workspace. Publish required source, reviews and runtime evidence through versioned Gitea commits or attachments before handing them to peers; verify access and link the exact version. Local paths are provenance only for remote peers. Local compiler, process and native-window observations do not establish another machine’s state or a shared hardware lease.
|
||||
|
||||
|
||||
## Saved Scenario spacing gestures — 2026-09-13
|
||||
|
||||
Codex-ytr83r /root. For the next Saved Scenario editing increment, dragging a
|
||||
lifeline header changes that view's lane x; dragging a message body or label
|
||||
changes that view's occurrence send-y. Preserve shared Scenario order and all
|
||||
other saved layout records. Keep semantic reordering explicit in the sequence
|
||||
outline; do not infer it from crossing another drawn message. Automatic views
|
||||
retain their existing derived-layout editing behavior.
|
||||
|
||||
This follows the user's requirement for directly editable diagrams and the
|
||||
[existing sparse layout contract](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/aed07176ab453b981d5f9c76e504c531f6dd7f42/harness/scenario-layout-contract-candidate.md).
|
||||
The [native Capella comparison](experiments/capella-scenario-observations-20260913/README.md)
|
||||
provides bounded primary evidence: receiver-execution movement changed only the
|
||||
diagram file; separate message/lane Undo restored both files exactly. Two arrow-body
|
||||
drag attempts did not move that example, so use the drawn body/label as discoverable
|
||||
Studio targets while distinguishing activation bars and lifecycle markers by role.
|
||||
This observation does not establish Capella's general crossing or reordering rules.
|
||||
|
||||
Reject implicit shared reordering on a Saved spacing drag: another view's unchanged
|
||||
coordinate records do not ensure unchanged appearance when automatic anchors
|
||||
recompute. Reject a generic permutation of message coordinates: actions, fragments,
|
||||
opaque references and previously crossed positions do not supply interchangeable
|
||||
slots. Reject resetting arrangements to automatic because it discards authored intent.
|
||||
|
||||
Preview from one captured accepted Project and visible camera; resolve the complete
|
||||
candidate geometry without mutating source or history. Commit one existing command
|
||||
only after a valid release and successful source update. Invalid previews, cancellation
|
||||
and return to origin preserve accepted values and camera; an ordinary click still
|
||||
selects its semantic target. Preserve explicit zero/empty/absent overrides and real
|
||||
Redo. Feasible source-order crossings remain diagnostic, not silently clamped.
|
||||
|
||||
Implementation and native acceptance remain pending at this decision. Require
|
||||
actual same-batch and multi-frame input, stale-token cancellation, preview/commit
|
||||
geometry agreement, one Undo/Redo and source reopen, plus measured large-model drag
|
||||
cost. Revisit target affordances or event flow if these workflows reveal accidental
|
||||
reordering, hidden target ambiguity, camera jumps or unacceptable preview latency.
|
||||
|
||||
+9
-7
@@ -1,12 +1,14 @@
|
||||
# Integrated checkpoint — 2026-09-11
|
||||
# Integrated checkpoint — 2026-09-12
|
||||
|
||||
Codex-ytr83r, isolated branch `codex/canvas-union-ytr83r-20260911`. Current source `cb91525` adds the three-file grapheme shortening correction to the combined `a57c334b302eb13352b4bb65df1c75d94d002348` source checkpoint; its remaining Windows gates and matching native checks remain pending. The a57/108b checkpoint evidence remains preserved; this source statement is not a release claim. Main checkpoint `32334c6` / production `865483b` and its existing package remain preserved. The full [user goal](../PROJECT_GOAL.md) remains incomplete. Live [canvas integration](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2), [native/release acceptance](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5) and the [project overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1) retain ownership and plans. This file scopes code and evidence.
|
||||
Codex-ytr83r, isolated branch `codex/canvas-union-ytr83r-20260911`. Current source `33e30c1` combines the a57 canvas, cb91525 grapheme and narrow saved-destination repair corrections. Its 530 frozen inputs match HEAD; independent repair review found no actionable finding. Windows desktop tests, strict Clippy, GUI build and bounded matching native/package acceptance passed. The [canvas-repair Windows prerelease](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.canvas-repair.20260912) is published; ZIP, checksum and validation attachments were downloaded and hash-verified. [Publication record](release-canvas-repair-publication-20260912.md) preserves the exact identities. This is not a full [user-goal](../PROJECT_GOAL.md) completion claim. Live [canvas integration](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2), [native/release acceptance](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5), [saved-destination repair](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/6) and [project overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1) retain ownership and plans. Earlier production `865483b` and its package remain preserved.
|
||||
|
||||
## Verified code and capabilities
|
||||
|
||||
Source `cb91525` changes the shared display helper to preserve extended grapheme clusters and records the direct locked dependency edge. Independent source review found no remaining actionable finding; four focused shortening tests passed at 20:59:36.0538079 UTC, with 752 filtered. Full desktop, strict Clippy, build and matching native checks remain pending in this record. The corrected 529-file input freeze supersedes only the premature pre-resolution freeze, against which no tests/build ran. [Correction, exact freezes and limits](release-grapheme-shortening-ytr83r-20260911.md), [verbatim independent review](release-grapheme-shortening-review-20260911.md).
|
||||
Source `33e30c1` binds repair dirty state to the actual saved destination, preserving accepted-model history even when Apply is an identical-content no-op. It selectively incorporates peer `33c6d83` in three files. Windows focused repair **5**, full desktop **761**, strict desktop all-targets Clippy and GUI build passed; the five are included in 761. All **530 files / 9,727,025 bytes** remained unchanged between gates and matched HEAD after the build. The preserved GUI `50486253…` passed matching repair/history/recovery and Unicode selection/passive-save checks, packaged walkthrough/help smoke and normal exit0 with empty logs. All559 package files stayed unchanged; fresh extracted server25-request editing/persistence checks passed. [Current validation and artifact hashes](release-canvas-repair-validation-20260912.md) links the exact raw archives and independent reviews. [Repair union and limits](release-repair-union-ytr83r-20260912.md), [closed gates and exact inputs](experiments/repair-union-windows-33e30c1-20260912/README.md), [verbatim independent review](release-saved-destination-independent-review-20260912.md).
|
||||
|
||||
The isolated `a57c334` union selectively combines peer `a31b40d` canvas/chooser/form prerequisites with the preserved B4 lifetime renderer, contextual source/minimum-card corrections and responsibility-preserving child-work creation. Pan-aware header/Create clips and pointer inversion received independent source review. Windows lifecycle **27**, full desktop **752**, strict desktop all-target Clippy and Studio build passed; all 529 frozen inputs matched HEAD and remained unchanged. The new unoptimized GUI is `69DBB90F…`; matching a57 native checks are in progress locally; complete acceptance remains pending. [Canvas union checkpoint, source/gate archives and exact limits](release-canvas-union-ytr83r-20260911.md).
|
||||
Source `cb91525` preserves extended grapheme clusters in the shared display helper. Its four focused tests, full desktop 756 and strict desktop Clippy passed; the four overlap the 756. Its GUI build was intentionally superseded by the repair union; matching combined33e native Unicode evidence is recorded above, without reattributing that result to cb91525. [Correction and limits](release-grapheme-shortening-ytr83r-20260911.md), [closed Windows evidence](experiments/grapheme-windows-cb91525-20260911/README.md).
|
||||
|
||||
The isolated `a57c334` union selectively combines peer `a31b40d` canvas/chooser/form prerequisites with B4 lifetime rendering and contextual source/child-work corrections. Windows lifecycle **27**, full desktop **752**, strict desktop Clippy and Studio build passed on 529 unchanged frozen inputs. GUI `69DBB90F…` passed bounded staged source/consumer creation, Cancel, Save, Undo/Redo and reopen checks: 13 oracles and 45 screenshots at the default wide window only. [Canvas checkpoint](release-canvas-union-ytr83r-20260911.md), [staged archive](experiments/staged-source-native-a57c334-20260911/README.md), [independent review](release-staged-native-a57-independent-review-20260911.md). A fresh a57 camera session subsequently passed bounded OA 100%/125%/Fit and minimum 1100×700 client Fit66 checks, OA/SA pan and moved Create/Delete hit selection, exact passive Save equality and normal exit 0. The 18 images support bounded clipping without toolbar bleed; labels are small at 66%. [Camera archive and limits](experiments/canvas-camera-a57c334-20260912/README.md), [verbatim independent review](release-camera-a57-independent-review-20260912.md). The [earlier picker failure](experiments/canvas-native-close-a57c334-20260911/README.md) remains historical evidence. These pre-grapheme a57 results do not establish current `33e30c1` native or complete usability acceptance.
|
||||
|
||||
Earlier published checkpoint `3ade397008cffe826d5f5a70e7c0233159e3bd42` contains production `29084050a7c020e0fb488be37c6e1ab235769cc0`; push and exact remote readback passed. It combines prior named behavior creation/history integration (`c81e285`, `e207b04`), corrected creation-oracle evidence (`b853a1e`), CLI-only integration (`023e3a8`) and B4 lifecycle rendering.
|
||||
|
||||
@@ -34,13 +36,13 @@ The closed 321897f Windows gate passed 704 desktop tests with zero failed/ignore
|
||||
|
||||
The later 865483b Windows desktop gate passed **707 tests**, including the three focused child-work regressions, with zero failed/ignored/filtered. All 261 recorded build inputs stayed unchanged. The matching debug GUI build and strict **desktop-only** all-targets Clippy passed; child-work and staged-source checker compilation/admission also passed. [Closed gates and exact inputs](experiments/windows-gates-865483b-20260911/README.md). These checks do not establish the remaining native rendering or packaged acceptance.
|
||||
|
||||
The extracted package's unchanged server `ABA87A5F…` passed the actual HTTP schema checks (204/19/9) and 25-request REST editing/persistence/restart probe with a restricted process PATH and unrelated working directory. Seven raw response pairs and exact source snapshots matched; all five test-owned server processes stopped. The first REST run's PowerShell-added PATH directory is preserved as historical evidence; the corrected restricted-PATH rerun is authoritative. [Packaged server evidence](experiments/packaged-server-865483b-20260911/README.md). This completes bounded server runtime acceptance, while the new Windows preview remains unpublished pending the separate GUI checks.
|
||||
The extracted package's unchanged server `ABA87A5F…` passed the actual HTTP schema checks (204/19/9) and 25-request REST editing/persistence/restart probe with a restricted process PATH and unrelated working directory. Seven raw response pairs and exact source snapshots matched; all five test-owned server processes stopped. The first REST run's PowerShell-added PATH directory is preserved as historical evidence; the corrected restricted-PATH rerun is authoritative. [Packaged server evidence](experiments/packaged-server-865483b-20260911/README.md). This completed the earlier bounded server runtime acceptance. Fresh33e package/server/GUI results are linked above; the old receipts retain their own source attribution.
|
||||
|
||||
## Material limitations
|
||||
|
||||
Earlier native wide Latin/CJK checks found neutral lifeline header text overflowing its card while Create labels stayed contained. The [header correction](lifecycle-header-overflow-correction-ytr83r-20260911.md) is included in 865483b and retained in a57. Actual corrected wide-W/CJK observations in BF9 show bounded headers and Create labels, including selected zoom, scroll and minimum-window cases. Both complete wide and cluster sources remained unchanged after passive Save. Cluster checks exposed a separate scalar-shortening defect: an accent or joined emoji can be split at the label cutoff. Full Unicode presentation and matching a57 native acceptance remain open; see the [native typography record](experiments/lifecycle-header-correction-native-865483b-20260911/README.md).
|
||||
Earlier native wide Latin/CJK checks found neutral lifeline header text overflowing its card while Create labels stayed contained. The [header correction](lifecycle-header-overflow-correction-ytr83r-20260911.md) is included in 865483b and retained in a57. Actual corrected wide-W/CJK observations in BF9 show bounded headers and Create labels, including selected zoom, scroll and minimum-window cases. Both complete wide and cluster sources remained unchanged after passive Save. Cluster checks exposed a separate scalar-shortening defect: an accent or joined emoji can be split at the label cutoff. The later cb91525 correction and current33e accent/ZWJ selection/passive-save checks resolve the observed string-splitting case. Long wide labels can still be hard-clipped and arbitrary font/DPI presentation is unverified; see the historical [native typography record](experiments/lifecycle-header-correction-native-865483b-20260911/README.md).
|
||||
|
||||
The earlier capture failures and pending Open dialog are historical: later confirmed OA and SA reopen/Save actions are preserved in the [continuation archive](experiments/native-child-continuation-865483b-20260911/README.md). Its first OA `reopened-1948` receipts remain data-only evidence; they are not promoted to successful-open observations. Contextual-source workflows, matching new-canvas pan/zoom/clipping/focus behavior and complete combined native/package acceptance remain unfinished. The BF9 Home page, scenario guide and bundled water-pressure example opened; one normal Alt+F4 then completed, with the process absent and empty stdout/stderr. The [closed Home/Guide/normal-close archive](experiments/packaged-gui-close-865483b-20260911/README.md) preserves these bounded old865/BF9 observations and complete package readback. No application exit code was captured; no exit0 or new-source native pass is inferred.
|
||||
The earlier capture failures and pending Open dialog are historical: later confirmed OA and SA reopen/Save actions are preserved in the [continuation archive](experiments/native-child-continuation-865483b-20260911/README.md). Its first OA `reopened-1948` receipts remain data-only evidence. Bounded a57 staged and camera workflows above are complete; full focus/keyboard, every clipping edge/zoom and accessibility remain unverified. Bounded combined `33e30c1` native/package acceptance is now linked above. Small 66% labels are a recorded limitation. The BF9 Home page, scenario guide and bundled water-pressure example opened; one normal Alt+F4 then completed, with the process absent and empty stdout/stderr. The [old Home/Guide/close archive](experiments/packaged-gui-close-865483b-20260911/README.md) preserves those old865/BF9 observations and package readback. No exit code was captured for that older BF9 process; the separate a57 exit 0 does not change its evidence.
|
||||
|
||||
The selected chooser/canvas union now has a57 source and Windows gate evidence; broader persistent graphical manipulation and separate text-navigation contributions retain their own integration/acceptance boundaries. Calculations, quantities/units, maintained libraries, practical large-model textual workflows, complete OA↔SA engineering parity, documentation and fresh cross-disciplinary review remain full-goal requirements. See the [analysis proposal](release-analysis-semantics-design-20260910.md), [library proposal](release-libraries-semantics-design-20260910.md), [text-authoring design](release-textual-authoring-design-20260911.md) and [decisions](DECISIONS.md) for attributed design evidence, not completed implementation claims.
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Attachment editing: Escape correction and verified scope
|
||||
|
||||
Branch-specific evidence for [issue 33](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/33),
|
||||
source `53d01a3d4451b94792986f2dfac56f6f04e111a4`.
|
||||
The [portable archive](experiments/edge-attachments-53d01a3-20260923/README.md)
|
||||
is authoritative for raw verification evidence and its limitations. The original
|
||||
[attachment decision](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/53d01a3d4451b94792986f2dfac56f6f04e111a4/harness/edge-attachments-20260922.md)
|
||||
and language contract remain authoritative for representation and behavior.
|
||||
|
||||
## Native finding and correction
|
||||
|
||||
Opening the State transition context menu resets the canvas wrapper's keyboard
|
||||
ownership. Selecting Adjust attachment arms the tool without giving the canvas
|
||||
focus. Therefore immediate Escape can reach the application's fallback while
|
||||
the unfocused canvas does not consume it. The original fallback omitted this
|
||||
tool, leaving it armed until a canvas interaction established ownership.
|
||||
|
||||
The accepted correction resets only State AdjustAttachment to Select in the
|
||||
existing application Escape fallback, after prior menu/modal/form handling.
|
||||
It sends no model command and grants no keyboard ownership. Granting focus after
|
||||
the menu would also change Delete and other keyboard semantics; the bounded
|
||||
fallback avoids that unrelated change. Semantic Reconnect policy is unchanged.
|
||||
|
||||
Mounted regression tests exercise actual widget statuses and application
|
||||
subscriptions, including post-menu Escape without a canvas click, focused
|
||||
canvas cancellation, modal priority and unfocused Delete. Full document/source,
|
||||
history and camera invariants are checked where relevant. Native source and
|
||||
target cancellation, source-end attachment editing/history and save/reopen then confirmed
|
||||
the corrected executable's bounded behavior. The preserved original failed
|
||||
observation is the reason for the correction, not superseded evidence to delete.
|
||||
|
||||
## Evidence handling decision
|
||||
|
||||
Build evidence and a native session accidentally shared an indirect executable
|
||||
binding through the Cargo target directory. The native preflight detected the
|
||||
replacement and stopped. Preserving both artifacts, documenting their hashes,
|
||||
restoring the original referenced bytes, and using a new comparison retained
|
||||
the evidence without rewriting history. The corrected native stage binds a
|
||||
preserved executable outside that mutable target directory. Future native
|
||||
stages should similarly separate immutable tested artifacts from build outputs;
|
||||
after restoring an executable, Cargo freshness metadata alone cannot establish
|
||||
that it matches the desired candidate.
|
||||
|
||||
Both portable archives preserve raw failures and use explicit source mappings
|
||||
instead of publishing machine-only paths. No entire-workflow, continuous
|
||||
immutability or clean-build claim is inferred from matching endpoint hashes.
|
||||
|
||||
## Integration boundary
|
||||
|
||||
The ordered contribution is `79cc9d5`, `3aa80de`, then `53d01a3`, above ordinary/Text
|
||||
base `3f9e6a18`. Source and evidence branches are separate. The
|
||||
[integration audit](experiments/edge-attachments-53d01a3-20260923/integration-review.md)
|
||||
identifies actual shared-file seams and focused combined checks. Codex-956atf
|
||||
retains integration/release ownership. These results do not certify the combined
|
||||
Scenario/Text/transition/resize union; no integrated checkpoint is advanced here.
|
||||
@@ -0,0 +1 @@
|
||||
* -text -whitespace
|
||||
@@ -0,0 +1,25 @@
|
||||
# Direct Allocation drawing — Windows evidence
|
||||
|
||||
Verified source: [301126f6098286bf440c1bd1b1ed685753984723](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/301126f6098286bf440c1bd1b1ed685753984723), branch `codex/allocation-drawing-ytr83r-20260923`. It adds the Allocation tool to eligible architecture projections through the existing guarded two-endpoint drawing workflow. See [source scope and decisions](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/301126f6098286bf440c1bd1b1ed685753984723/harness/allocation-drawing-20260923.md) and [Gitea issue30](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/30).
|
||||
|
||||
Windows gates passed: **1,222 desktop tests, 0 failures, 6 existing ignored**, strict desktop all-target Clippy, matching GUI and snapshot evaluator builds. All six new Allocation tests passed. The six gate steps recorded unchanged source inputs. Unchanged core/server workspace evidence is reused with separate attribution to b868541/evidence d6aabb5; this is not a new workspace suite.
|
||||
|
||||
Root exercised OA Rescue team → Assess incident and SA Rescue system → Evaluate report using the right-side Allocation tool. Creation, Undo/Redo, occupied OA target refusal and Escape cancellation, Save and fresh reopen produced **nine exact complete Project/source-bundle checkpoints**. The two relation UUIDs were bound once; every other field was fixed in advance. Both normal closes were observed, with process exit0, empty GUI logs and complete saved bundle/identity continuity. The accepted trial contains26 capture records. [Parent observations](reviews/allocation-drawing-parent-acceptance-20260923.md), [independent final review](reviews/allocation-drawing-native-final-review-20260923.md), and [explicit coverage](acceptance-coverage.json) distinguish live actions, archived review and automated comparisons.
|
||||
|
||||
The [native recipe](native/README.md) describes the fixture, commands and oracle. Expected fixture-completeness warnings are retained; the final fixture still lacks behavior context. Other projections and stale/draft/invalid-target cases have automated/source coverage, not claimed native coverage. This is branch-specific Windows evidence, not combined integration, release, whole-project, accessibility or every-window-size acceptance. Linux remains user-paused.
|
||||
|
||||
## Verify the portable evidence
|
||||
|
||||
Fetch source commits301126f, cf37c36 and17f1f89 in a repository, then run from this directory with Python3:
|
||||
|
||||
```text
|
||||
python verify-build.py verify --archive . --repo /path/to/ARCADIA-Studio
|
||||
```
|
||||
|
||||
`archive-manifest.json` maps unchanged copied originals; `source-map.json` maps captured source bytes to their exact Git commits (or explicit byte-matching overrides). Raw records retain historical machine paths, but the portable verifier resolves copied evidence and Git objects without those paths. `external-reference-inventory.json` explicitly classifies omitted binary/runtime and private/mutable references. This verifies evidence bytes and provenance; it does not reproduce omitted executable bytes or re-execute the product. The separately reused source converter remains attributed to17f1f89 and identical core/Cargo objects.
|
||||
|
||||
Collection contains604 unchanged originals (56,849,882 bytes) and11,036 source records, all matched exactly to Git with no reconstructed overrides or historical source gaps. The accepted trial has28 image files for26 capture records. All4,457 remaining external references are explicitly omitted binary/runtime records (1,080 unique byte/hash pairs); no unresolved nonbinary reference was admitted.
|
||||
|
||||
Failed gate `gates/failed-final-01` remains a compilation failure at cf37c36, with no executed tests. `partial-interrupted-native/` preserves the first trial's four accepted OA checkpoints separately; its missing close and unknown exit cause are explained in [the interruption note](partial-interrupted-native-note.md). It is not successful close/reopen or crash evidence. The fresh accepted trial is under `native/`.
|
||||
|
||||
This archive contains evidence and reproduction recipes, not a runnable release. Integration/release ownership remains [issue27](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/27); no other session can consume unpublished local files.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"checkpoints": [
|
||||
{"path":"runs/run-01/checkpoints/00-baseline/receipt.json","expected":"B"},
|
||||
{"path":"runs/run-01/checkpoints/01-oa-created/receipt.json","expected":"O"},
|
||||
{"path":"runs/run-01/checkpoints/02-oa-undo/receipt.json","expected":"B"},
|
||||
{"path":"runs/run-01/checkpoints/03-oa-redo/receipt.json","expected":"O"},
|
||||
{"path":"runs/run-01/checkpoints/04-occupied-cancel/receipt.json","expected":"O"},
|
||||
{"path":"runs/run-01/checkpoints/05-sa-created/receipt.json","expected":"OS"},
|
||||
{"path":"runs/run-01/checkpoints/06-sa-undo/receipt.json","expected":"O"},
|
||||
{"path":"runs/run-01/checkpoints/07-sa-redo/receipt.json","expected":"OS"},
|
||||
{"path":"runs/run-02/checkpoints/08-reopen/receipt.json","expected":"OS"}
|
||||
],
|
||||
"final_checkpoint_by_run": {
|
||||
"1":"runs/run-01/checkpoints/07-sa-redo/receipt.json",
|
||||
"2":"runs/run-02/checkpoints/08-reopen/receipt.json"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
"""Collect closed #30 evidence into a fresh archive; original files stay read-only."""
|
||||
from pathlib import Path
|
||||
import argparse,importlib.util,json,subprocess,hashlib,sys,datetime,copy,uuid
|
||||
sys.dont_write_bytecode=True
|
||||
HERE=Path(__file__).resolve().parent;ROOT=HERE.parent.parent
|
||||
spec=importlib.util.spec_from_file_location('portable',ROOT/'.local/edge-anchors-evidence-20260923/verify-build.py');h=importlib.util.module_from_spec(spec);spec.loader.exec_module(h)
|
||||
PIN='301126f6098286bf440c1bd1b1ed685753984723';OLD='cf37c36be16500a4a86622540395aaafb1645f6a';BASE='17f1f89fef4a6bf520f4b09a2116548b967d030e'
|
||||
REPO=ROOT/'.local/allocation-drawing-20260923';GATES=ROOT/'.local/allocation-drawing-gates-20260923'
|
||||
OMIT={'.exe','.dll','.pdb','.rlib','.rmeta','.lib','.a'};SKIP={'profile','working','package','unrelated-cwd','__pycache__','artifacts','target'}
|
||||
INTERRUPTED_SCOPE='Partial interrupted native trial; checkpoint evidence only. No normal-close, exit-zero, reopen acceptance, or application-crash inference.'
|
||||
GAPS=set() # Both candidate inventories and reused17f converter source must resolve exactly.
|
||||
def validate_native(native,coverage):
|
||||
if (native/'UNACCEPTED.json').exists():raise RuntimeError('Quarantined native trial')
|
||||
catalog=h.read(native/'expected/catalog.json');contract=h.read(catalog['contract']['path']);baseline=h.read(catalog['checkpoints']['B']['path'])
|
||||
bindings={key:h.read(native/(key+'-binding.json')) for key in ('OA','SA')}
|
||||
for key,binding in bindings.items():
|
||||
rid=binding['relation_uuid'];assert str(uuid.UUID(rid))==rid and uuid.UUID(rid).version==4
|
||||
first=h.read(binding['first_receipt']['path']);assert first['accepted'] and first['new_bindings'][key]==rid
|
||||
assert bindings['OA']['relation_uuid']!=bindings['SA']['relation_uuid']
|
||||
entries=coverage['checkpoints']
|
||||
if not entries or set(x['expected'] for x in entries)!={'B','O','OS'}:raise RuntimeError('Explicit B/O/OS acceptance coverage required')
|
||||
paths=[x['path'] for x in entries]
|
||||
if len(paths)!=len(set(paths)):raise RuntimeError('Duplicate checkpoint')
|
||||
for item in entries:
|
||||
path=(native/item['path']).resolve()
|
||||
if not path.is_relative_to(native):raise RuntimeError('Checkpoint outside native root')
|
||||
r=h.read(path)
|
||||
assert r['accepted'] is True and not r['failure'] and r['source_commit']==PIN and r['expected']==item['expected']
|
||||
assert r['original_before']==r['original_after']
|
||||
expected=copy.deepcopy(baseline)
|
||||
for key in contract['states'][r['expected']]:
|
||||
rid=bindings[key]['relation_uuid'];relation=copy.deepcopy(contract['relations'][key]);relation['id']=rid;expected['relations'][rid]=relation
|
||||
assert h.read(r['actual_project']['path'])==expected
|
||||
assert len(r['commands'])==2 and all(x['exit_code']==0 for x in r['commands'])
|
||||
for name,record in r['snapshot_after'].items():
|
||||
target=(path.parent/name).resolve()
|
||||
if not target.is_relative_to(path.parent):raise RuntimeError('Snapshot path escapes checkpoint')
|
||||
raw=target.read_bytes();assert len(raw)==record['bytes'] and h.digest(raw)==record['sha256']
|
||||
for record in h.records(r):
|
||||
if Path(record['path']).suffix.lower() in OMIT:continue
|
||||
raw=Path(record['path']).read_bytes();assert len(raw)==record['bytes'] and h.digest(raw)==record['sha256']
|
||||
for run,receipt_path in coverage['final_checkpoint_by_run'].items():
|
||||
assert receipt_path in paths
|
||||
checkpoint=h.read(native/receipt_path);close=h.read(native/'runs'/f'run-{int(run):02d}'/'normal-close.json')
|
||||
assert checkpoint['accepted'] and checkpoint['expected']=='OS'
|
||||
bundle={Path(x['path']).relative_to(native/'working').as_posix():{k:x[k] for k in ('bytes','sha256')} for x in close['working_bundle']}
|
||||
assert bundle==checkpoint['original_after']
|
||||
for record in close['relation_bindings_after']:
|
||||
raw=Path(record['path']).read_bytes();assert len(raw)==record['bytes'] and h.digest(raw)==record['sha256']
|
||||
if set(coverage['final_checkpoint_by_run'])!={'1','2'}:raise RuntimeError('Both planned closes require explicit final OS checkpoint')
|
||||
return {'accepted_checkpoints':entries,'scope':'Built-in complete bundle conversion and exact B/O/OS Project checks; only OA/SA UUID parameters, fixed once; final close coverage explicitly declared'}
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser();p.add_argument('--output',required=True);p.add_argument('--reviews-finalized',action='store_true');p.add_argument('--run',type=int,action='append',required=True);p.add_argument('--native',type=Path,required=True);p.add_argument('--coverage',required=True,type=Path);p.add_argument('--converter-admission',required=True,type=Path);p.add_argument('--review',type=Path,action='append',required=True);p.add_argument('--interrupted-native',type=Path);p.add_argument('--interruption-note',type=Path);a=p.parse_args();NATIVE=a.native.resolve()
|
||||
if not a.reviews_finalized:raise RuntimeError('Parent/reviewer final confirmation required')
|
||||
out=Path(a.output).resolve()
|
||||
if out.exists():raise RuntimeError('Fresh archive required')
|
||||
summary=h.read(GATES/'final-02/summary.json');assert summary['source_commit']==PIN and summary['passed'] and summary['inputs_unchanged']
|
||||
for attempt,pin in [('final-01',OLD)]:
|
||||
failed=h.read(GATES/attempt/'summary.json');assert failed['source_commit']==pin and not failed['passed'] and failed['inputs_unchanged']
|
||||
stage=h.read(NATIVE/'staging-record.json');assert stage['source_commit']==PIN
|
||||
assert h.digest((NATIVE/'input-manifest.json').read_bytes())==stage['manifest']['sha256']
|
||||
assert h.read(NATIVE/'input-manifest.json')['source_commit']==PIN
|
||||
interrupted_native=a.interrupted_native.resolve() if a.interrupted_native else None
|
||||
if bool(interrupted_native)!=bool(a.interruption_note):raise RuntimeError('Interrupted native root and standalone note must be supplied together')
|
||||
if interrupted_native:
|
||||
if interrupted_native==NATIVE:raise RuntimeError('Interrupted and accepted native roots must be distinct')
|
||||
partial_stage=h.read(interrupted_native/'staging-record.json')
|
||||
assert partial_stage['source_commit']==PIN and h.digest((interrupted_native/'input-manifest.json').read_bytes())==partial_stage['manifest']['sha256']
|
||||
assert not list((interrupted_native/'runs').glob('*/normal-close.json'))
|
||||
assert h.read(interrupted_native/'OA-binding.json')['relation_uuid']=='9dc63df6-ce83-47ee-be58-bcfa9d13482c'
|
||||
for record in h.records(h.read(interrupted_native/'OA-binding.json')):
|
||||
raw=Path(record['path']).read_bytes();assert len(raw)==record['bytes'] and h.digest(raw)==record['sha256']
|
||||
for checkpoint,expected in [('00-baseline','B'),('01-oa-created','O'),('02-oa-undo','B'),('03-oa-redo','O')]:
|
||||
checkpoint_root=interrupted_native/'runs/run-01/checkpoints'/checkpoint;partial=h.read(checkpoint_root/'receipt.json')
|
||||
assert partial['source_commit']==PIN and partial['accepted'] is True and not partial['failure'] and partial['expected']==expected and partial['original_before']==partial['original_after']
|
||||
for name,record in partial['snapshot_after'].items():
|
||||
target=(checkpoint_root/name).resolve();assert target.is_relative_to(checkpoint_root)
|
||||
raw=target.read_bytes();assert len(raw)==record['bytes'] and h.digest(raw)==record['sha256']
|
||||
if not a.run or len(set(a.run))!=len(a.run) or any(n<1 for n in a.run):raise RuntimeError('Unique explicit run IDs required')
|
||||
for n in sorted(a.run):
|
||||
folder=NATIVE/'runs'/f'run-{n:02d}';close=h.read(folder/'normal-close.json');launch=h.read(folder/'launch.json')
|
||||
assert close['source_commit']==PIN and close['exit_code']==0 and close['immutable_inputs_unchanged'] and close['stdout']['bytes']==close['stderr']['bytes']==0
|
||||
assert close['launch_sha256']==h.digest((folder/'launch.json').read_bytes()) and close['pid']==launch['pid'] and close['started_utc']==launch['started_utc']
|
||||
if set(a.run)!={1,2}:raise RuntimeError('Both planned normally closed native runs must be included')
|
||||
close1=h.read(NATIVE/'runs/run-01/normal-close.json');launch2=h.read(NATIVE/'runs/run-02/launch.json')
|
||||
assert launch2['previous_close_sha256']==h.digest((NATIVE/'runs/run-01/normal-close.json').read_bytes())
|
||||
assert launch2['working_bundle_before']==close1['working_bundle'] and launch2['relation_bindings_before']==close1['relation_bindings_after']
|
||||
coverage=h.read(a.coverage);bundle_review=validate_native(NATIVE,coverage)
|
||||
converter=h.read(a.converter_admission);assert converter['approved'] and converter['source_commit']==BASE
|
||||
converter_inventories=[Path(r['path']) for r in converter['records'] if Path(r['path']).name in ('inputs-before.json','inputs-after.json')]
|
||||
if not converter_inventories:raise RuntimeError('Raw converter source inventories required')
|
||||
out.mkdir();inventory=[];source=[];available=set();seen_original=set()
|
||||
def copy(src,rel):
|
||||
src=Path(src).resolve();raw=src.read_bytes();dest=out/rel;dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
with dest.open('xb') as f:f.write(raw)
|
||||
assert src.read_bytes()==dest.read_bytes()
|
||||
item={'original':str(src),'archive':rel,'bytes':len(raw),'sha256':h.digest(raw)};inventory.append(item);available.add((len(raw),h.digest(raw)));seen_original.add(str(src));return item
|
||||
def tree(folder,prefix,exclude_runs=False):
|
||||
for src in sorted(folder.rglob('*')):
|
||||
rel=src.relative_to(folder)
|
||||
if src.is_symlink() or src.is_junction() or getattr(src.lstat(),'st_file_attributes',0)&1024:raise RuntimeError('Linked archive input')
|
||||
if src.is_file() and src.suffix.lower() not in OMIT and not any(s in SKIP for s in rel.parts) and not (exclude_runs and rel.parts[0]=='runs'):copy(src,prefix+'/'+rel.as_posix())
|
||||
tree(GATES/'final-01','gates/failed-final-01');tree(GATES/'final-02','gates/final');tree(NATIVE,'native',exclude_runs=True)
|
||||
if interrupted_native:
|
||||
tree(interrupted_native,'partial-interrupted-native')
|
||||
copy(a.interruption_note,'partial-interrupted-native-note.md')
|
||||
copy(a.coverage,'acceptance-coverage.json');copy(a.converter_admission,'converter-admission.json')
|
||||
for n in sorted(a.run):tree(NATIVE/'runs'/f'run-{n:02d}','native/runs/'+f'run-{n:02d}')
|
||||
for name in ('run.py','final.py','README.md'):copy(GATES/name,'gates/recipes/'+name)
|
||||
if len({path.name for path in a.review})!=len(a.review):raise RuntimeError('Review filenames must be unique')
|
||||
for path in a.review:copy(path,'reviews/'+path.name)
|
||||
copy(ROOT/'.local/edge-anchors-evidence-20260923/verify-build.py','verify-build.py');copy(__file__,'collect.py')
|
||||
# Map each frozen gate inventory only to its own exact source commit.
|
||||
# Reuse Git content rather than duplicating historical archives inside source.
|
||||
blobs={}
|
||||
for pin in (PIN,OLD,BASE):
|
||||
paths=subprocess.check_output(['git','-c','core.longpaths=true','-C',str(REPO),'ls-tree','-rz','--name-only',pin]).decode().rstrip('\0').split('\0');blobs[pin]=dict(h.git_blobs(REPO,pin,paths))
|
||||
missing=[];derived=[]
|
||||
inventories=[('final-02',PIN,GATES/'final-02/inputs-before.json',REPO),('final-01',OLD,GATES/'final-01/inputs-before.json',REPO)]+[('converter-'+str(i),BASE,path,ROOT/'.local/traceability-view-20260923') for i,path in enumerate(converter_inventories)]
|
||||
for snapshot,testedpin,manifest,source_repo in inventories:
|
||||
for item in h.read(manifest):
|
||||
path=Path(item['path']).relative_to(source_repo).as_posix();match=None
|
||||
for pin in [testedpin]:
|
||||
if path not in blobs[pin]:continue
|
||||
raw=blobs[pin][path]
|
||||
for method,data in [('git',raw),('reconstructed-crlf',raw.replace(b'\r\n',b'\n').replace(b'\n',b'\r\n'))]:
|
||||
if len(data)==item['bytes'] and h.digest(data)==item['sha256']:match=(pin,method,data);break
|
||||
if match:break
|
||||
if match is None:
|
||||
raise RuntimeError('Unexpected unmapped source: '+snapshot+' '+path)
|
||||
pin,method,data=match;row={'original':item['path'],'git_path':path,'commit':pin,'bytes':item['bytes'],'sha256':item['sha256'],'captured_snapshot':snapshot,'tested_base_commit':testedpin}
|
||||
if method=='git':row['storage']='git'
|
||||
else:
|
||||
rel='source-overrides/'+snapshot+'/'+path;dest=out/rel;dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
with dest.open('xb') as f:f.write(data)
|
||||
row.update(storage='override',archive=rel,derivation='Git blob LF normalized then CRLF expanded; reconstructed bytes exactly match captured receipt hash')
|
||||
derived.append(row)
|
||||
source.append(row);available.add((item['bytes'],item['sha256']))
|
||||
# Resolve small exact referenced originals (env/runner/etc) into portable copies.
|
||||
# Never copy mismatched mutable content or binary/private files to hide a gap.
|
||||
for iteration in range(4):
|
||||
refs=[r for item in inventory if item['archive'].endswith('.json') for r in h.records(h.read(out/item['archive']))];added=0
|
||||
for r in refs:
|
||||
if (r['bytes'],r['sha256']) in available or r['sha256'] in GAPS:continue
|
||||
src=Path(r['path'])
|
||||
if src.suffix.lower() in OMIT or any(s in SKIP for s in src.parts):continue
|
||||
if src.is_file() and str(src.resolve()) not in seen_original:
|
||||
raw=src.read_bytes()
|
||||
if len(raw)==r['bytes'] and h.digest(raw)==r['sha256']:
|
||||
copy(src,'dependencies/'+f'{len(inventory):04d}-'+src.name);added+=1
|
||||
if not added:break
|
||||
h.write(out/'source-map.json',source);h.write(out/'historical-source-gaps.json',missing)
|
||||
refs=[dict(receipt=item['archive'],**r) for item in inventory if item['archive'].endswith('.json') for r in h.records(h.read(out/item['archive']))]
|
||||
unresolved=[r for r in refs if (r['bytes'],r['sha256']) not in available]
|
||||
classified=[]
|
||||
for r in unresolved:
|
||||
category='omitted-binary-runtime' if Path(r['path']).suffix.lower() in OMIT else ('omitted-private-or-mutable-directory' if any(s in SKIP for s in Path(r['path']).parts) else ('unavailable-failed-targeted-source' if r['sha256'] in GAPS else 'UNEXPECTED'))
|
||||
classified.append(dict(category=category,**r))
|
||||
if any(r['category']=='UNEXPECTED' for r in classified):h.write(HERE/'unexpected-references.json',classified);raise RuntimeError('Unexpected nonbinary reference; inspect report, preserve incomplete archive')
|
||||
h.write(out/'external-reference-inventory.json',classified)
|
||||
h.write(out/'archive-manifest.json',{'original_receipts_unchanged':True,'files':inventory,'source_map':'source-map.json','source_map_sha256':h.digest((out/'source-map.json').read_bytes()),'source_commit':PIN,'source_bundle_supplement':bundle_review,'selected_closed_runs':sorted(a.run),'interrupted_native_included':bool(interrupted_native),'interrupted_native_scope':INTERRUPTED_SCOPE if interrupted_native else None,'historical_source_gaps_sha256':h.digest((out/'historical-source-gaps.json').read_bytes()),'external_reference_inventory_sha256':h.digest((out/'external-reference-inventory.json').read_bytes()),'excluded':'Executable/library/debug binaries and directories named profile, working, package, unrelated-cwd, __pycache__, artifacts, or target. Both gate and reused converter source inventories must resolve exactly; no unavailable-source allowances. This archive verifies evidence, not omitted executable bytes or a runnable release.'})
|
||||
(out/'.gitattributes').write_text('* -text -whitespace\n',encoding='ascii')
|
||||
images=[p for p in (out/'native/runs').rglob('*') if p.suffix.lower() in ('.jpg','.jpeg','.png')]
|
||||
report={'source_commit':PIN,'source_bundle_supplement':bundle_review,'selected_closed_runs':sorted(a.run),'interrupted_native_included':bool(interrupted_native),'interrupted_native_scope':INTERRUPTED_SCOPE if interrupted_native else None,'raw_originals':len(inventory),'raw_bytes':sum(r['bytes'] for r in inventory),'source_records':len(source),'git_exact':sum(r['storage']=='git' for r in source),'reconstructed_crlf_overrides':len(derived),'historical_source_gaps':len(missing),'image_count':len(images),'distinct_images':len({h.digest(p.read_bytes()) for p in images}),'external_reference_count':len(classified),'external_unique':len({(r['bytes'],r['sha256']) for r in classified}),'external_categories':{c:sum(r['category']==c for r in classified) for c in sorted({r['category'] for r in classified})}}
|
||||
h.write(HERE/(out.name+'-collection-report.json'),report);print(json.dumps(report,indent=2))
|
||||
if __name__=='__main__':main()
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"approved": true,
|
||||
"source_commit": "17f1f89fef4a6bf520f4b09a2116548b967d030e",
|
||||
"declared_utc": "2026-09-23T09:09:01.078753+00:00",
|
||||
"binary": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\package\\bundle_converter.exe",
|
||||
"bytes": 24271360,
|
||||
"sha256": "70743dfd3e0d6ab077bb2db5447d74fe7884b010bb48913525436385c05c8c70"
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-bundle-build-review-20260923.md",
|
||||
"bytes": 3423,
|
||||
"sha256": "8d51601f0cd1e226d5af3e35e6eb91fb209dc0eb24d131060a2cd02b02eb7f7b"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-bundle-supplement-review-20260923.md",
|
||||
"bytes": 13603,
|
||||
"sha256": "166ef4d4e448a045c738acbc60c5372eccce455c079e9edf1d21f671d24a3816"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\admit_converter.py",
|
||||
"bytes": 1675,
|
||||
"sha256": "ab65621a9b7581692001b1a8726319c76d48cc0977f049424e9b84ffcb41fc1f"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\cargo-artifact.json",
|
||||
"bytes": 1467,
|
||||
"sha256": "8ebac3a0b6b19acbf18bd86dc28c29348f6e0cb7f944a6696fd5b22c86d116b4"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\cargo-lib.result.json",
|
||||
"bytes": 930,
|
||||
"sha256": "6e49dd0790f0c3f02834b40f21190b2b9a27009685be125822e38d89d372ab57"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\cargo-lib.stderr.log",
|
||||
"bytes": 60,
|
||||
"sha256": "31c37829b9fef7111400cb1ca99f523600d13210ea15e2d868b2442ef0f66db9"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\cargo-lib.stdout.log",
|
||||
"bytes": 39557,
|
||||
"sha256": "9585453d627b26e19e611ba51ab7ed70fff903887f75d8580c48a4c3f9b5ba98"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\converter-build.result.json",
|
||||
"bytes": 1691,
|
||||
"sha256": "d8c97d15b9054e2e22a005710569ebde42e2cb542391565d782ea2c93228a9c4"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\converter-build.stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\converter-build.stdout.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\environment.json",
|
||||
"bytes": 6398,
|
||||
"sha256": "e287ac67c96eaa03211992e1696f90659907a452fb09ae579525f31d1a930eef"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\inputs-after.json",
|
||||
"bytes": 823347,
|
||||
"sha256": "a784149396e248c45ee39f945e042cd269fa1d7b7538e411dd2f81219c6b1c8e"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\inputs-before.json",
|
||||
"bytes": 823347,
|
||||
"sha256": "a784149396e248c45ee39f945e042cd269fa1d7b7538e411dd2f81219c6b1c8e"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\libraries-after.json",
|
||||
"bytes": 279176,
|
||||
"sha256": "bb41d6cc66a3f85bfee021bef2be5c7801adf588ce265017e033616af112f625"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\libraries-before.json",
|
||||
"bytes": 279176,
|
||||
"sha256": "bb41d6cc66a3f85bfee021bef2be5c7801adf588ce265017e033616af112f625"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\package\\bundle_converter.exe",
|
||||
"bytes": 24271360,
|
||||
"sha256": "70743dfd3e0d6ab077bb2db5447d74fe7884b010bb48913525436385c05c8c70"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\package\\liboa_core.rlib",
|
||||
"bytes": 100490486,
|
||||
"sha256": "0a02043cb059fd18e41b603877c6397c5a299c6fb3b66587f8474ae21fd43d27"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\package\\libunwind.dll",
|
||||
"bytes": 190464,
|
||||
"sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-sysroot.result.json",
|
||||
"bytes": 847,
|
||||
"sha256": "d09ee1d7caa1cc7b1c6887530cad6f12095b56e9351a3645da7766c9f123cb8a"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-sysroot.stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-sysroot.stdout.log",
|
||||
"bytes": 111,
|
||||
"sha256": "ea05ec29f82bab8f02772e420e6a2d97608b985680d2d7fea2eaafebe317e479"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-version.result.json",
|
||||
"bytes": 851,
|
||||
"sha256": "a6032cd359df5bc2a772aed83cd81809ed935793219c0381c4ba5d58d60a139e"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-version.stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\rustc-version.stdout.log",
|
||||
"bytes": 197,
|
||||
"sha256": "cdaaa400be47332d69e678836251d4635b5ff2ecd34aa962b69d71f8961d274b"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build-01\\summary.json",
|
||||
"bytes": 5964,
|
||||
"sha256": "5f2f4f16310e21a9121184e0cf97e5d0d2f09a0e2cfcf72176c231b2c86b50f3"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\build_converter.py",
|
||||
"bytes": 7081,
|
||||
"sha256": "29ffe083606d55d9577222fb32684010a164d26a87224281646df2d335fa8a87"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\bundle_converter.rs",
|
||||
"bytes": 2279,
|
||||
"sha256": "acbcc955e970e47b4e825f7880af2fa70ca679410ee4d765d28623ea646c4bf6"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\capture_bundle.py",
|
||||
"bytes": 8237,
|
||||
"sha256": "588736077c67b44aff423146ef2891d9f7816f744fb6356f7e4ec619d5b95a2b"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\traceability-view-native-17f1f89-20260923-02\\supplement-source-bundle-v2\\README.md",
|
||||
"bytes": 7148,
|
||||
"sha256": "64dc99eca333a16873f7377d7ae87458d529a1dbeea8409082663047b1c8cdcb"
|
||||
}
|
||||
],
|
||||
"scope": "Parent accepts independently reviewed auxiliary build for read-only saved-bundle conversion. Original product gates, evaluator and B/P oracle unchanged. Cargo cache reuse is explicit; runtime checkpoint still required."
|
||||
}
|
||||
+31201
File diff suppressed because it is too large
Load Diff
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"artifacts": [],
|
||||
"scope": "Original shared-target locations are historical; native stage must bind preserved copies"
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:40:43.832935+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T09:41:12.387449+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\cargo-version\\output.log",
|
||||
"bytes": 324,
|
||||
"sha256": "74e615b1a23cdf066aa39466d65195469cf81608585010359201fd306adbaa51"
|
||||
}
|
||||
}
|
||||
cargo 1.98.1 (797e8a9bc 2026-08-05)
|
||||
release: 1.98.1
|
||||
commit-hash: 797e8a9bca276c1c9f9f738d2a20f484fa4eea9d
|
||||
commit-date: 2026-08-05
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
libgit2: 1.9.4 (sys:0.21.0 vendored)
|
||||
libcurl: 8.21.0-DEV (sys:0.4.90+curl-8.21.0 vendored ssl:Schannel)
|
||||
os: Windows 10.0.26200 (Windows 11 Professional) [64-bit]
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:40:43.832935+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
+13802
File diff suppressed because it is too large
Load Diff
+8
@@ -0,0 +1,8 @@
|
||||
cargo 1.98.1 (797e8a9bc 2026-08-05)
|
||||
release: 1.98.1
|
||||
commit-hash: 797e8a9bca276c1c9f9f738d2a20f484fa4eea9d
|
||||
commit-date: 2026-08-05
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
libgit2: 1.9.4 (sys:0.21.0 vendored)
|
||||
libcurl: 8.21.0-DEV (sys:0.4.90+curl-8.21.0 vendored ssl:Schannel)
|
||||
os: Windows 10.0.26200 (Windows 11 Professional) [64-bit]
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:40:43.832935+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T09:41:12.387449+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\cargo-version\\output.log",
|
||||
"bytes": 324,
|
||||
"sha256": "74e615b1a23cdf066aa39466d65195469cf81608585010359201fd306adbaa51"
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"test",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:41:45.403846+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 101,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T10:11:36.021194+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\desktop-tests\\output.log",
|
||||
"bytes": 3175,
|
||||
"sha256": "8c255b407304bedc299c43767269715b5d0c1730e384b3eec8bda271abe0a4ba"
|
||||
}
|
||||
}
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-core)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-server)
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\vendor\iced_widget)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-desktop)
|
||||
Compiling iced v0.14.0
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:24:41
|
||||
|
|
||||
24 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:231:45
|
||||
|
|
||||
231 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:289:41
|
||||
|
|
||||
289 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:323:49
|
||||
|
|
||||
323 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:405:41
|
||||
|
|
||||
405 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
For more information about this error, try `rustc --explain E0308`.
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 5 previous errors
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"test",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:41:45.403846+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
+13802
File diff suppressed because it is too large
Load Diff
+77
@@ -0,0 +1,77 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-core)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-server)
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\vendor\iced_widget)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-desktop)
|
||||
Compiling iced v0.14.0
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:24:41
|
||||
|
|
||||
24 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:231:45
|
||||
|
|
||||
231 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:289:41
|
||||
|
|
||||
289 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:323:49
|
||||
|
|
||||
323 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
error[E0308]: mismatched types
|
||||
--> crates\oa-desktop\src\connection_allocation_tests.rs:405:41
|
||||
|
|
||||
405 | let _ = app.update(Message::Diagram(diagram));
|
||||
| ---------------- ^^^^^^^ expected `Choice`, found `Uuid`
|
||||
| |
|
||||
| arguments to this enum variant are incorrect
|
||||
|
|
||||
note: tuple variant defined here
|
||||
--> crates\oa-desktop\src\app.rs:417:5
|
||||
|
|
||||
417 | Diagram(Choice),
|
||||
| ^^^^^^^
|
||||
|
||||
For more information about this error, try `rustc --explain E0308`.
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 5 previous errors
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"test",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:41:45.403846+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 101,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T10:11:36.021194+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\desktop-tests\\output.log",
|
||||
"bytes": 3175,
|
||||
"sha256": "8c255b407304bedc299c43767269715b5d0c1730e384b3eec8bda271abe0a4ba"
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"environment": {
|
||||
"PATH": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\bin;C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\powershell;C:\\Users\\User\\.codex\\tmp\\arg0\\codex-arg0efKbE4;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\libheif\\libheif\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\jxrlib\\jxrlib\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\poppler\\Library\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\bin\\override;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Program Files\\Git\\cmd;C:\\Users\\User\\AppData\\Local\\Programs\\OpenAI\\Codex\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\powershell;C:\\Users\\User\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\User\\AppData\\Local\\Programs\\Zed\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\bin\\fallback;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\node\\bin;C:\\Users\\User\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\native\\git\\cmd;C:\\Users\\User\\AppData\\Local\\OpenAI\\Codex\\bin\\6fdec9b372c2c0fd;C:\\Users\\User\\AppData\\Local\\OpenAI\\Codex\\bin\\7ac07f4ce733f89a",
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe",
|
||||
"RUSTFLAGS": null,
|
||||
"CARGO_ENCODED_RUSTFLAGS": null
|
||||
},
|
||||
"env_script": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\env.ps1",
|
||||
"bytes": 572,
|
||||
"sha256": "be2226f08764bb6961b4be8513ce55bab076890cddd2fef0f54a4031f94a82d4"
|
||||
},
|
||||
"cargo_configs": [
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\.cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\.cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\config",
|
||||
"present": false,
|
||||
"record": null
|
||||
},
|
||||
{
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\config.toml",
|
||||
"present": false,
|
||||
"record": null
|
||||
}
|
||||
],
|
||||
"cargo_resolved": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\bin\\cargo.EXE",
|
||||
"rustc_resolved": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\bin\\rustc.EXE",
|
||||
"runtime": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\libunwind.dll",
|
||||
"bytes": 190464,
|
||||
"sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
},
|
||||
"orchestrator": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final.py",
|
||||
"bytes": 8349,
|
||||
"sha256": "f57021cd9b88efbda439d9e3df8bfe9f96193c48375a308493fe23f89717d2f0"
|
||||
},
|
||||
"command_runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
+13802
File diff suppressed because it is too large
Load Diff
+92
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"passed": false,
|
||||
"inputs_unchanged": true,
|
||||
"started_utc": "2026-09-23T09:38:47.681949+00:00",
|
||||
"finished_utc": "2026-09-23T10:11:46.084856+00:00",
|
||||
"failure": "Final gate failed: desktop-tests",
|
||||
"steps": [
|
||||
{
|
||||
"name": "toolchain",
|
||||
"argv": [
|
||||
"rustc",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"runner_exit_code": 0,
|
||||
"orchestration_log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\toolchain.orchestration.log",
|
||||
"bytes": 1744,
|
||||
"sha256": "95b3f5829a0c94f9317d75a94c8edf15f69e213e729c90abfb126e5ab8c915de"
|
||||
},
|
||||
"result": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\toolchain\\result.json",
|
||||
"bytes": 1538,
|
||||
"sha256": "74027cae19247b6156bd9ad955b78d6b71390600063dae1c60997e1310474d61"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cargo-version",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"runner_exit_code": 0,
|
||||
"orchestration_log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\cargo-version.orchestration.log",
|
||||
"bytes": 1876,
|
||||
"sha256": "8e064efc060049f888904588d70f7ee897640ea29f9ecaae643bf627e286ad7f"
|
||||
},
|
||||
"result": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\cargo-version\\result.json",
|
||||
"bytes": 1542,
|
||||
"sha256": "96f4ce16e6c871ecc3e25df7fed605e26be5d58edb48df5da70cb8c9050be3a4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "desktop-tests",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"test",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"runner_exit_code": 101,
|
||||
"orchestration_log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\desktop-tests.orchestration.log",
|
||||
"bytes": 4841,
|
||||
"sha256": "76d77575690a1b8a8ca62646a9c6570d4bf67dfe90f17008c6bfd23918d72b6e"
|
||||
},
|
||||
"result": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\desktop-tests\\result.json",
|
||||
"bytes": 1587,
|
||||
"sha256": "485357d17cae5643014959c8130f0df8c8f7234807c502df01123a16398b8404"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source_manifest": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\inputs-before.json",
|
||||
"bytes": 812830,
|
||||
"sha256": "0e0d3c9e346bf47722eef692aa0e551b24b80468c9813c024a468e2d34bf70d0"
|
||||
},
|
||||
"environment": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\environment.json",
|
||||
"bytes": 5832,
|
||||
"sha256": "c016659b127b9b8ee8448d5a7b24ad7325275f048381442db2b7a5e2ae11a420"
|
||||
},
|
||||
"relocation": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\artifact-relocation.json",
|
||||
"bytes": 188,
|
||||
"sha256": "f94d9ffe44a5277b5ae4e85b26c6e34a4e984991ad3b2e90dac6cc1247885eaf"
|
||||
},
|
||||
"reused_workspace_gate": {
|
||||
"source_commit": "b8685416f56e53e30bd11ecf93a54cea10ac8458",
|
||||
"evidence_commit": "d6aabb5dec82900ebfac1183abb4036ca06cc0e5",
|
||||
"scope": "Prior workspace core/server evidence; this run executes desktop gates and the unchanged core snapshot example only"
|
||||
},
|
||||
"scope": "Recorded desktop command exit codes and exact input inventories; no new workspace/core/server suite or native GUI acceptance"
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"rustc",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:39:21.578880+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T09:40:04.901807+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\toolchain\\output.log",
|
||||
"bytes": 197,
|
||||
"sha256": "cdaaa400be47332d69e678836251d4635b5ff2ecd34aa962b69d71f8961d274b"
|
||||
}
|
||||
}
|
||||
rustc 1.98.1 (48a229cea 2026-09-01)
|
||||
binary: rustc
|
||||
commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985
|
||||
commit-date: 2026-09-01
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
release: 1.98.1
|
||||
LLVM version: 22.1.8
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"rustc",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:39:21.578880+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
+13802
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
rustc 1.98.1 (48a229cea 2026-09-01)
|
||||
binary: rustc
|
||||
commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985
|
||||
commit-date: 2026-09-01
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
release: 1.98.1
|
||||
LLVM version: 22.1.8
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"source_commit": "cf37c36be16500a4a86622540395aaafb1645f6a",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"rustc",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T09:39:21.578880+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T09:40:04.901807+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-01\\toolchain\\output.log",
|
||||
"bytes": 197,
|
||||
"sha256": "cdaaa400be47332d69e678836251d4635b5ff2ecd34aa962b69d71f8961d274b"
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"artifacts": [
|
||||
{
|
||||
"original": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\target\\debug\\arcadia-studio.exe",
|
||||
"bytes": 117342720,
|
||||
"sha256": "59b0a90167bbfe170a501bd3cb959720b8496a2215cc84b269c1983a278cef03"
|
||||
},
|
||||
"preserved": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\artifacts\\arcadia-studio.exe",
|
||||
"bytes": 117342720,
|
||||
"sha256": "59b0a90167bbfe170a501bd3cb959720b8496a2215cc84b269c1983a278cef03"
|
||||
}
|
||||
},
|
||||
{
|
||||
"original": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\target\\debug\\examples\\attachment_snapshot.exe",
|
||||
"bytes": 18957824,
|
||||
"sha256": "d548c5c8c6428e8dc9a1f4a8599f08b1dc9f6529f188f07a192d3a7fa2494b35"
|
||||
},
|
||||
"preserved": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\artifacts\\attachment_snapshot.exe",
|
||||
"bytes": 18957824,
|
||||
"sha256": "d548c5c8c6428e8dc9a1f4a8599f08b1dc9f6529f188f07a192d3a7fa2494b35"
|
||||
}
|
||||
},
|
||||
{
|
||||
"original": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\libunwind.dll",
|
||||
"bytes": 190464,
|
||||
"sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
},
|
||||
"preserved": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\artifacts\\libunwind.dll",
|
||||
"bytes": 190464,
|
||||
"sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"scope": "Original shared-target locations are historical; native stage must bind preserved copies"
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T10:15:40.956702+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T10:15:45.741546+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\cargo-version\\output.log",
|
||||
"bytes": 324,
|
||||
"sha256": "74e615b1a23cdf066aa39466d65195469cf81608585010359201fd306adbaa51"
|
||||
}
|
||||
}
|
||||
cargo 1.98.1 (797e8a9bc 2026-08-05)
|
||||
release: 1.98.1
|
||||
commit-hash: 797e8a9bca276c1c9f9f738d2a20f484fa4eea9d
|
||||
commit-date: 2026-08-05
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
libgit2: 1.9.4 (sys:0.21.0 vendored)
|
||||
libcurl: 8.21.0-DEV (sys:0.4.90+curl-8.21.0 vendored ssl:Schannel)
|
||||
os: Windows 10.0.26200 (Windows 11 Professional) [64-bit]
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T10:15:40.956702+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
+13802
File diff suppressed because it is too large
Load Diff
+8
@@ -0,0 +1,8 @@
|
||||
cargo 1.98.1 (797e8a9bc 2026-08-05)
|
||||
release: 1.98.1
|
||||
commit-hash: 797e8a9bca276c1c9f9f738d2a20f484fa4eea9d
|
||||
commit-date: 2026-08-05
|
||||
host: x86_64-pc-windows-gnullvm
|
||||
libgit2: 1.9.4 (sys:0.21.0 vendored)
|
||||
libcurl: 8.21.0-DEV (sys:0.4.90+curl-8.21.0 vendored ssl:Schannel)
|
||||
os: Windows 10.0.26200 (Windows 11 Professional) [64-bit]
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"--version",
|
||||
"--verbose"
|
||||
],
|
||||
"started_utc": "2026-09-23T10:15:40.956702+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T10:15:45.741546+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\cargo-version\\output.log",
|
||||
"bytes": 324,
|
||||
"sha256": "74e615b1a23cdf066aa39466d65195469cf81608585010359201fd306adbaa51"
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"build",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"started_utc": "2026-09-23T11:50:06.675207+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"inputs_unchanged": true,
|
||||
"finished_utc": "2026-09-23T18:28:36.878929+00:00",
|
||||
"log": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\final-02\\desktop-build\\output.log",
|
||||
"bytes": 314,
|
||||
"sha256": "0fa864983ba646ffb0a8b17dfd56af7d72887e7e89b0a59f9be7e21f84ad7c64"
|
||||
}
|
||||
}
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-core)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\allocation-drawing-20260923\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 47m 40s
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"source_commit": "301126f6098286bf440c1bd1b1ed685753984723",
|
||||
"working_changes": "",
|
||||
"cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-20260923",
|
||||
"argv": [
|
||||
"cargo",
|
||||
"build",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--locked",
|
||||
"--offline"
|
||||
],
|
||||
"started_utc": "2026-09-23T11:50:06.675207+00:00",
|
||||
"environment": {
|
||||
"CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo",
|
||||
"RUSTUP_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\rustup",
|
||||
"RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm",
|
||||
"CARGO_TARGET_DIR": "C:/Users/User/Documents/Git/ARCADIA-Studio/target",
|
||||
"CARGO_BUILD_JOBS": "1",
|
||||
"CARGO_INCREMENTAL": "0",
|
||||
"CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER": "clang.exe",
|
||||
"RC": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\llvm-windres.exe"
|
||||
},
|
||||
"runner": {
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\allocation-drawing-gates-20260923\\run.py",
|
||||
"bytes": 1880,
|
||||
"sha256": "2d2630af45c0f850d724657823cb428a3e7101ddf9d3c51436e2aa1c759b7d66"
|
||||
}
|
||||
}
|
||||
+13802
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user