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, ) -> 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 }