Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions crates/sqlbuild-core/src/cli_text/_helpers/bounded_sections.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Bounded sections, aligned name/value rows and ANSI stripping, as `sqlbuild.presentation` does.

use crate::cli_text::constants::MIN_NAME_COLUMN_WIDTH;
use crate::cli_text::models::{CliStyle, DisplayLimit};

/// The text without ANSI SGR sequences (`ESC [ digits-and-semicolons m`).
pub(crate) fn strip_ansi(text: &str) -> String {
let mut stripped: String = String::with_capacity(text.len());
let mut rest: &str = text;
while let Some(start) = rest.find("\x1b[") {
stripped.push_str(&rest[..start]);
let after: &str = &rest[start + 2..];
let parameters: usize = after
.find(|character: char| !(character.is_ascii_digit() || character == ';'))
.unwrap_or(after.len());
if after[parameters..].starts_with('m') {
rest = &after[parameters + 1..];
} else {
stripped.push('\x1b');
rest = &rest[start + 1..];
}
}
stripped.push_str(rest);
stripped
}

/// `count_header_style` over `plan_section`: `Title (count)` as a bold title and dim count.
pub(crate) fn section_header(style: CliStyle, text: &str) -> String {
if let Some(split) = text.rfind(" (")
&& text.ends_with(')')
{
return format!(
"{} {}",
style.plan_section(&text[..split]),
style.muted(&text[split + 1..])
);
}
style.plan_section(text)
}

/// How many of `total` entries a section shows.
pub(crate) fn visible_count(total: usize, display: &DisplayLimit) -> usize {
display.max_entries.map_or(total, |limit| total.min(limit))
}

/// The dim `... and N more` line when a section hides entries.
pub(crate) fn overflow_line(
(total, visible): (usize, usize),
indent: &str,
display: &DisplayLimit,
) -> Option<String> {
(total > visible).then(|| {
CliStyle { use_color: true }.muted(&format!(
"{indent}... and {} more (use {} to show all)",
total - visible,
display.overflow_flag
))
})
}

/// The widest name, but at least the minimum name column.
pub(crate) fn name_column_width<'a>(names: impl Iterator<Item = &'a str>) -> usize {
names
.map(|name| name.chars().count())
.max()
.unwrap_or(0)
.max(MIN_NAME_COLUMN_WIDTH)
}

/// ` name<padding> value`, with the value dim unless `dim_value` is off.
pub(crate) fn name_value_line(name: &str, value: &str, width: usize, dim_value: bool) -> String {
let style: CliStyle = CliStyle { use_color: true };
let rendered: String = if dim_value {
style.muted(value)
} else {
value.to_owned()
};
let padding: String = " ".repeat(width.saturating_sub(name.chars().count()));
format!(" {}{padding} {rendered}", style.object_name(name))
}

/// A comma-separated name list cut to the section limit with a `+N more` note.
pub(crate) fn capped_names(
names: &[String],
display: &DisplayLimit,
name_style: impl Fn(&str) -> String,
) -> String {
let visible: usize = visible_count(names.len(), display);
let base: String = names[..visible]
.iter()
.map(|name| name_style(name))
.collect::<Vec<String>>()
.join(", ");
if visible >= names.len() {
return base;
}
format!(
"{base}, ... (+{} more; use {} to show all)",
names.len() - visible,
display.overflow_flag
)
}
4 changes: 4 additions & 0 deletions crates/sqlbuild-core/src/cli_text/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
pub(crate) mod bounded_sections;
pub(crate) mod diagnostics;
pub(crate) mod layout;
pub(crate) mod plan_document;
pub(crate) mod plan_sections;
pub(crate) mod tree_rows;
84 changes: 84 additions & 0 deletions crates/sqlbuild-core/src/cli_text/_helpers/plan_document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! The full-refresh and incremental bodies of the text plan.

use crate::cli_text::_helpers::layout::surface_header;
use crate::cli_text::_helpers::plan_sections::{
full_refresh_count_lines, functions_lines, plan_name_width, provider_usage_lines, pruned_lines,
python_entry_lines, ready_summary, reason_group_lines, remaining_stale_lines,
routine_model_lines, seed_lines, source_freshness_lines, source_load_lines, warning_lines,
};
use crate::cli_text::_helpers::tree_rows::treeify;
use crate::cli_text::constants::{
PLAN_READY_TITLE, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE, PYTHON_READ_SIDE_TITLE,
READ_SIDE_PHASE,
};
use crate::cli_text::models::{CliStyle, ModelPlanText, PlanTextReport};

fn header_lines(report: &PlanTextReport, full_refresh: bool) -> Vec<String> {
if !report.include_header {
return Vec::new();
}
vec![surface_header(
CliStyle { use_color: true },
PLAN_READY_TITLE,
&ready_summary(report, full_refresh),
)]
}

/// Every active model is a full refresh: counts by materialization instead of detail.
pub(crate) fn full_refresh_lines(
report: &PlanTextReport,
active: &[&ModelPlanText],
) -> Vec<String> {
let width: usize = plan_name_width(report);
let mut lines: Vec<String> = header_lines(report, true);
lines.extend(provider_usage_lines(report));
lines.extend(python_entry_lines(
(report, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE),
width,
));
lines.extend(source_load_lines(report, width));
lines.extend(functions_lines(report, true, width));
lines.extend(functions_lines(report, false, width));
if !lines.is_empty() {
lines.push(String::new());
}
lines.extend(full_refresh_count_lines(active));
lines.extend(seed_lines(report));
lines.extend(python_entry_lines(
(report, READ_SIDE_PHASE, PYTHON_READ_SIDE_TITLE),
width,
));
lines.extend(warning_lines(report));
lines.extend(report.pre_build_lines.iter().cloned());
lines
}

/// Sections by change reason, drawn as trees.
pub(crate) fn incremental_lines(report: &PlanTextReport, active: &[&ModelPlanText]) -> Vec<String> {
let width: usize = plan_name_width(report);
let mut lines: Vec<String> = header_lines(report, false);
if report.include_freshness_diagnostics {
lines.extend(source_freshness_lines(report));
}
lines.extend(remaining_stale_lines(report));
lines.extend(provider_usage_lines(report));
lines.extend(pruned_lines(report));
lines.extend(report.pre_build_lines.iter().cloned());
lines.extend(python_entry_lines(
(report, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE),
width,
));
lines.extend(source_load_lines(report, width));
lines.extend(functions_lines(report, true, width));
lines.extend(reason_group_lines(report, active, width));
lines.extend(routine_model_lines(report, active, width));
lines.extend(functions_lines(report, false, width));
lines.extend(seed_lines(report));
lines.extend(python_entry_lines(
(report, READ_SIDE_PHASE, PYTHON_READ_SIDE_TITLE),
width,
));
let mut lines: Vec<String> = treeify(lines);
lines.extend(warning_lines(report));
lines
}
Loading
Loading