261 lines
8.6 KiB
Rust
261 lines
8.6 KiB
Rust
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
|
|
}
|