fix: grow the hint panel for long prose hints
A long prose hint (insert field-value hints, the parse.usage.* synopses) wrapped but was clipped by the fixed one-row Hint panel, hiding the most useful tail. The candidate list already scrolled horizontally, so only prose was affected. Pre-wrap the prose body and size the Hint panel to the wrapped line count: one row by default, growing to a 3-row cap and reclaiming the space when short, with an ellipsis backstop on the last row. Also shorten the 299-char create-table usage synopsis to a terse one-liner (the full grammar remains under `help`). ADR-0022 Amendment 5.
This commit is contained in:
@@ -642,6 +642,50 @@ example), `advanced_double_precision_classified_as_type`,
|
||||
extended theme mapping/contrast tests. Text snapshots are colour-blind
|
||||
(`render_to_string` strips style), so none churned.
|
||||
|
||||
## Amendment 5 — Hint panel grows for long prose hints (2026-05-29)
|
||||
|
||||
§6 specified a single-row hint panel. A long **prose** hint
|
||||
(field-value hints on `insert`, the `parse.usage.*` synopses) simply
|
||||
didn't fit: the renderer wrapped it but the fixed one-row panel
|
||||
clipped everything past the first line, hiding the most useful tail
|
||||
(issue #12). The candidate-list path was already safe — it scrolls
|
||||
horizontally with `<` / `>` markers (Amendment 2's deferred *two-line
|
||||
candidate box* remains deferred; this amendment does **not** touch
|
||||
candidate rendering).
|
||||
|
||||
**Change:**
|
||||
|
||||
1. **Dynamic height.** `resolve_hint_lines` pre-wraps the prose body
|
||||
to the panel's inner width and `render_right_column` sizes the
|
||||
Hint panel to the line count — one content row by default, growing
|
||||
to `MAX_HINT_ROWS` (3), and reclaiming the space when the hint is
|
||||
short or empty. The candidate list stays one row.
|
||||
2. **Ellipsis backstop.** `clamp_wrapped` caps the wrap at
|
||||
`MAX_HINT_ROWS`; if the hint needs more, the last visible row ends
|
||||
with `…` so overflow is signalled, not silently dropped. (A hint
|
||||
long enough to ellipsize on a narrow terminal fits in full on a
|
||||
wide one.)
|
||||
3. **Shorter `create table` synopsis.** `parse.usage.sql_create_table`
|
||||
was a 299-char "instruction manual" — by far the longest hint
|
||||
string (next was 115). It is now a terse one-liner
|
||||
(`create table [if not exists] <Name> (<col> <type> [constraints],
|
||||
...)`); the exhaustive column- and table-level grammar still lives
|
||||
in `help.ddl.sql_create_table`, reachable via `help`. The
|
||||
`insert.form_b_*` notes (the other long strings) render in the
|
||||
scrollable Output panel, not the Hint panel, so they were left
|
||||
alone.
|
||||
|
||||
**Why 3 rows:** the panel is usually wide (most hints fit one or two
|
||||
rows), and capping at three keeps the Output panel usable; the value
|
||||
can be revisited if hints routinely need more.
|
||||
|
||||
**Coverage:** `long_prose_hint_shows_tail_across_multiple_rows`,
|
||||
`short_hint_keeps_panel_at_one_content_row` (reclaim),
|
||||
`long_hint_grows_panel_but_caps_at_max_rows`,
|
||||
`clamp_wrapped_truncates_with_ellipsis_past_max`, and six re-baselined
|
||||
full-screen snapshots (the empty-state placeholder and the `insert`
|
||||
usage hint now wrap to their full text instead of being clipped).
|
||||
|
||||
## Out of scope
|
||||
|
||||
Deliberately deferred to keep this ADR shippable as a single
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ This directory contains the project's ADRs, recorded per
|
||||
- [ADR-0019 — Friendly error layer (H1) and i18n message catalog](0019-friendly-error-layer-and-i18n.md)
|
||||
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md)
|
||||
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md)
|
||||
- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms
|
||||
- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms; **Amendment 5 lets the hint panel grow for long prose hints** — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); `resolve_hint_lines` now pre-wraps prose and `render_right_column` sizes the panel to the line count (1 row default, up to `MAX_HINT_ROWS`=3, reclaimed when short) with a `clamp_wrapped` ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char `parse.usage.sql_create_table` synopsis to a terse one-liner (full grammar remains in `help.ddl.sql_create_table`)
|
||||
- [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024)
|
||||
- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note)
|
||||
- [ADR-0025 — Indexes](0025-indexes.md) — **Accepted** (**Amendment 1, 2026-05-25**: UNIQUE indexes admitted on the **advanced-mode** surface via `CREATE UNIQUE INDEX` — ADR-0035 §4d; the `IndexSchema.unique` flag round-trips through `project.yaml` with no new metadata table since the engine reports uniqueness natively; simple-mode `add unique index` stays deferred), `add index` / `drop index`, persistence, rebuild-table preservation, and items-list display (`C3` index portion + `S2`)
|
||||
|
||||
@@ -486,7 +486,9 @@ parse:
|
||||
# placeholders. ADR-0009's surface conventions apply.
|
||||
usage:
|
||||
create_table: "create table <Name> with pk [<col>(<type>)[, ...]]"
|
||||
sql_create_table: "create table [if not exists] <Name> (<col> <type> [not null] [unique] [primary key] [default <expr>] [check (<expr>)] [references <Parent>[(<col>)]], ... [, primary key (<col>, ...)] [, unique (<col>, ...)] [, check (<expr>)] [, [constraint <name>] foreign key (<col>) references <Parent>[(<col>)]])"
|
||||
# Terse one-line synopsis (issue #12): the full grammar — every
|
||||
# column- and table-level constraint — lives in `help.ddl.sql_create_table`.
|
||||
sql_create_table: "create table [if not exists] <Name> (<col> <type> [constraints], ...)"
|
||||
sql_drop_table: "drop table [if exists] <Name>"
|
||||
sql_create_index: "create [unique] index [if not exists] [<Name>] on <Table> (<col>[, ...])"
|
||||
sql_drop_index: "drop index [if exists] <Name>"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1540
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -16,13 +17,13 @@ expression: snapshot
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ ADVANCED ────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││Type a command — press Tab for options, `help` for│
|
||||
│ ││Type a command — press Tab for options, `help` │
|
||||
│ ││for a list │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · mode simple switch · Ctrl-C quit
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1523
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -16,13 +17,13 @@ expression: snapshot
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││Type a command — press Tab for options, `help` for│
|
||||
│ ││Type a command — press Tab for options, `help` │
|
||||
│ ││for a list │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1531
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -16,13 +17,13 @@ expression: snapshot
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││Type a command — press Tab for options, `help` for│
|
||||
│ ││Type a command — press Tab for options, `help` │
|
||||
│ ││for a list │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1583
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -15,14 +16,14 @@ expression: snapshot
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ ││insert into T values (1, 'hi', null) --all-r │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││after `insert into T values (1, 'hi', null)`, │
|
||||
│ ││expected end of input — usage: insert into │
|
||||
│ ││<Table> [(<col>[, ...])] [values] (<value>[, ...])│
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1771
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -16,13 +17,13 @@ expression: snapshot
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││Type a command — press Tab for options, `help` for│
|
||||
│ ││Type a command — press Tab for options, `help` │
|
||||
│ ││for a list │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: src/ui.rs
|
||||
assertion_line: 1469
|
||||
assertion_line: 1613
|
||||
expression: snapshot
|
||||
---
|
||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||
@@ -17,13 +17,13 @@ expression: snapshot
|
||||
│ │ │ │
|
||||
│ │Continue? │ │
|
||||
│ │ │ │
|
||||
│ │[Y] Yes [N] No Esc cancel │ │
|
||||
│ ╰──────────────────────────────────────────────────────────╯─────────╯
|
||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||
│ │[Y] Yes [N] No Esc cancel │─────────╯
|
||||
│ ╰──────────────────────────────────────────────────────────╯─────────╮
|
||||
│ ││ │
|
||||
│ │╰──────────────────────────────────────────────────╯
|
||||
│ │╭ Hint ────────────────────────────────────────────╮
|
||||
│ ││Type a command — press Tab for options, `help` for│
|
||||
│ ││Type a command — press Tab for options, `help` │
|
||||
│ ││for a list │
|
||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||
Project: Term Planner
|
||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||
|
||||
@@ -493,6 +493,36 @@ fn wrap_lines(s: &str, width: usize) -> Vec<String> {
|
||||
lines
|
||||
}
|
||||
|
||||
/// Maximum content rows the Hint panel may grow to before its last
|
||||
/// visible row is ellipsis-truncated (issue #12). The panel starts
|
||||
/// at one row and grows only as far as a wrapped hint needs, up to
|
||||
/// this cap, reclaiming the space when the hint is short.
|
||||
const MAX_HINT_ROWS: usize = 3;
|
||||
|
||||
/// Word-wrap `text` to `width`, then clamp to at most `max_rows`
|
||||
/// rows. If wrapping produced more rows than the cap, the last kept
|
||||
/// row is truncated to end with an ellipsis so the overflow is
|
||||
/// signalled rather than silently dropped (issue #12). Every
|
||||
/// returned row fits within `width`.
|
||||
fn clamp_wrapped(text: &str, width: usize, max_rows: usize) -> Vec<String> {
|
||||
let mut lines = wrap_lines(text, width);
|
||||
if lines.len() <= max_rows {
|
||||
return lines;
|
||||
}
|
||||
lines.truncate(max_rows.max(1));
|
||||
if let Some(last) = lines.last_mut() {
|
||||
// Reserve one column for the ellipsis.
|
||||
let budget = width.saturating_sub(1);
|
||||
let mut chars: Vec<char> = last.chars().collect();
|
||||
while chars.len() > budget {
|
||||
chars.pop();
|
||||
}
|
||||
chars.push('…');
|
||||
*last = chars.into_iter().collect();
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let label_style = Style::default().fg(theme.muted);
|
||||
let value_style = Style::default()
|
||||
@@ -521,18 +551,27 @@ fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: R
|
||||
}
|
||||
|
||||
fn render_right_column(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
// Resolve the hint first so the layout can size the Hint panel to
|
||||
// the wrapped hint (issue #12): one content row by default,
|
||||
// growing up to MAX_HINT_ROWS, reclaiming the space when short.
|
||||
// The hint panel spans the full column width, so `area.width` is
|
||||
// its width too.
|
||||
let hint_lines = resolve_hint_lines(app, theme, area.width);
|
||||
let hint_content =
|
||||
(hint_lines.len().clamp(1, MAX_HINT_ROWS) as u16).saturating_add(2);
|
||||
|
||||
let rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(5), // Output panel
|
||||
Constraint::Length(3), // Input panel
|
||||
Constraint::Length(3), // Hint panel
|
||||
Constraint::Min(5), // Output panel
|
||||
Constraint::Length(3), // Input panel
|
||||
Constraint::Length(hint_content), // Hint panel (dynamic)
|
||||
])
|
||||
.split(area);
|
||||
|
||||
render_output_panel(app, theme, frame, rows[0]);
|
||||
render_input_panel(app, theme, frame, rows[1]);
|
||||
render_hint_panel(app, theme, frame, rows[2]);
|
||||
render_hint_panel(theme, frame, rows[2], hint_lines);
|
||||
}
|
||||
|
||||
fn paint_background(theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
@@ -932,31 +971,40 @@ fn strip_one_shot_prefix(input: &str, cursor: usize) -> (&str, usize) {
|
||||
(effective, effective_cursor)
|
||||
}
|
||||
|
||||
fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(theme.border))
|
||||
.title(Span::styled(
|
||||
format!(" {} ", crate::t!("panel.hint_title")),
|
||||
Style::default()
|
||||
.fg(theme.fg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||
/// Resolve the Hint panel body into its rendered lines, pre-wrapped
|
||||
/// to the panel's inner width and clamped to `MAX_HINT_ROWS` with an
|
||||
/// ellipsis backstop (issue #12). The returned line count is the
|
||||
/// content-row count `render_right_column` allocates for, so the
|
||||
/// panel grows for a long hint and reclaims the space for a short
|
||||
/// one.
|
||||
///
|
||||
/// Resolution order for the body:
|
||||
/// 1. An explicit app-set hint (e.g. modal contexts) wins.
|
||||
/// 2. Otherwise, with non-empty input, the ambient
|
||||
/// typing-assistance hint (ADR-0022 §6) in the effective mode.
|
||||
/// 3. Otherwise, the empty-state placeholder.
|
||||
///
|
||||
/// Prose hints (1, the ambient `Prose` arm, and the placeholder)
|
||||
/// word-wrap across up to `MAX_HINT_ROWS` rows. The candidate list
|
||||
/// stays a single row and scrolls horizontally with `<` / `>`
|
||||
/// markers (`render_candidate_line`) — it already self-fits, so it
|
||||
/// is not wrapped.
|
||||
///
|
||||
/// ADR-0022 Amendment 1: advanced mode no longer skips ambient
|
||||
/// hinting. The original §12 carve-out predated the unified
|
||||
/// mode-aware walker (ADR-0030/0031/0032); the walker now speaks
|
||||
/// SQL, so `ambient_hint_in_mode` surfaces SQL slot hints +
|
||||
/// completion candidates in advanced mode too.
|
||||
fn resolve_hint_lines(app: &App, theme: &Theme, area_width: u16) -> Vec<Line<'static>> {
|
||||
let inner = area_width.saturating_sub(2) as usize;
|
||||
let muted = Style::default().fg(theme.muted);
|
||||
let prose = |text: &str| {
|
||||
clamp_wrapped(text, inner, MAX_HINT_ROWS)
|
||||
.into_iter()
|
||||
.map(|l| Line::from(Span::styled(l, muted)))
|
||||
.collect::<Vec<Line<'static>>>()
|
||||
};
|
||||
|
||||
// Resolution order for the hint panel body:
|
||||
// 1. An explicit app-set hint (e.g. modal contexts) wins.
|
||||
// 2. Otherwise, with non-empty input, the ambient
|
||||
// typing-assistance hint (ADR-0022 §6) computed in the
|
||||
// effective mode.
|
||||
// 3. Otherwise, the existing empty-state placeholder.
|
||||
// ADR-0022 Amendment 1: advanced mode no longer skips ambient
|
||||
// hinting. The original §12 carve-out predated the unified
|
||||
// mode-aware walker (ADR-0030/0031/0032); the walker now
|
||||
// speaks SQL, so `ambient_hint_in_mode` surfaces SQL slot
|
||||
// hints + completion candidates in advanced mode too.
|
||||
let empty_hint = crate::t!("panel.hint_empty");
|
||||
// In one-shot advanced mode (`:` prefix in simple mode) the
|
||||
// raw input carries the `:` sigil, which is not part of the
|
||||
// grammar. Strip it for the ambient computation so the hint
|
||||
@@ -974,27 +1022,37 @@ fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect
|
||||
&app.schema_cache,
|
||||
app.effective_mode().as_mode(),
|
||||
);
|
||||
let muted = Style::default().fg(theme.muted);
|
||||
let line = match (app.hint.as_deref(), ambient) {
|
||||
(Some(set), _) => Line::from(Span::styled(set.to_string(), muted)),
|
||||
(None, Some(crate::input_render::AmbientHint::Prose(text))) => {
|
||||
Line::from(Span::styled(text, muted))
|
||||
}
|
||||
match (app.hint.as_deref(), ambient) {
|
||||
(Some(set), _) => prose(set),
|
||||
(None, Some(crate::input_render::AmbientHint::Prose(text))) => prose(&text),
|
||||
(None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => {
|
||||
// ADR-0022 §7 + user's #2: render items with the
|
||||
// selected one highlighted; if not all fit,
|
||||
// scroll horizontally with `<` / `>` markers at
|
||||
// the edges. Inner width = panel area minus
|
||||
// borders (2).
|
||||
let inner = area.width.saturating_sub(2) as usize;
|
||||
render_candidate_line(&items, selected, inner, theme)
|
||||
vec![render_candidate_line(&items, selected, inner, theme)]
|
||||
}
|
||||
(None, None) => Line::from(Span::styled(empty_hint, muted)),
|
||||
};
|
||||
let paragraph = Paragraph::new(line)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false });
|
||||
(None, None) => prose(&crate::t!("panel.hint_empty")),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_hint_panel(
|
||||
theme: &Theme,
|
||||
frame: &mut Frame<'_>,
|
||||
area: Rect,
|
||||
lines: Vec<Line<'static>>,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(theme.border))
|
||||
.title(Span::styled(
|
||||
format!(" {} ", crate::t!("panel.hint_title")),
|
||||
Style::default()
|
||||
.fg(theme.fg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||
|
||||
// Lines are already wrapped to the inner width by
|
||||
// `resolve_hint_lines`, so no Paragraph-level wrapping is needed.
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -1371,6 +1429,93 @@ mod tests {
|
||||
out
|
||||
}
|
||||
|
||||
// ---- Issue #12: long hints no longer clipped to one row -----
|
||||
|
||||
const LONG_HINT: &str = "(id, created_at auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)";
|
||||
|
||||
#[test]
|
||||
fn long_prose_hint_shows_tail_across_multiple_rows() {
|
||||
// Before the fix the Hint panel was a fixed 1 content row,
|
||||
// so this hint's useful tail was clipped. Now the panel
|
||||
// grows (to MAX_HINT_ROWS) so the tail is visible.
|
||||
let mut app = App::new();
|
||||
app.hint = Some(LONG_HINT.to_string());
|
||||
let theme = Theme::dark();
|
||||
let out = render_to_string(&mut app, &theme, 80, 20);
|
||||
assert!(
|
||||
out.contains("list columns explicitly"),
|
||||
"the hint tail must be visible, not clipped:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_hint_keeps_panel_at_one_content_row() {
|
||||
// Reclaim: a short hint must not inflate the panel.
|
||||
let mut app = App::new();
|
||||
app.hint = Some("Type a command".to_string());
|
||||
let theme = Theme::dark();
|
||||
let out = render_to_string(&mut app, &theme, 80, 20);
|
||||
assert!(
|
||||
out.lines().any(|l| l.contains("Type a command")),
|
||||
"short hint visible:\n{out}"
|
||||
);
|
||||
assert_eq!(
|
||||
hint_content_rows(&out),
|
||||
1,
|
||||
"short hint should occupy exactly one content row:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_hint_grows_panel_but_caps_at_max_rows() {
|
||||
let mut app = App::new();
|
||||
app.hint = Some(LONG_HINT.to_string());
|
||||
let theme = Theme::dark();
|
||||
// Narrow width forces more wrapped lines than the cap.
|
||||
let out = render_to_string(&mut app, &theme, 44, 20);
|
||||
assert_eq!(
|
||||
hint_content_rows(&out),
|
||||
MAX_HINT_ROWS,
|
||||
"long hint caps at MAX_HINT_ROWS content rows:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Count the content rows inside the Hint panel of a rendered
|
||||
/// screen: the rows between the `╭ Hint …` title border and the
|
||||
/// next `╰…╯` bottom border.
|
||||
fn hint_content_rows(out: &str) -> usize {
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
let top = lines
|
||||
.iter()
|
||||
.position(|l| l.contains("Hint") && l.contains('╭'))
|
||||
.expect("hint title border present");
|
||||
// Rows strictly between the title border and the next
|
||||
// bottom border == the content-row count.
|
||||
lines[top + 1..]
|
||||
.iter()
|
||||
.position(|l| l.contains('╰'))
|
||||
.expect("hint bottom border present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamp_wrapped_truncates_with_ellipsis_past_max() {
|
||||
// ≤ max rows: untouched.
|
||||
let two = clamp_wrapped("alpha beta gamma delta", 11, 3);
|
||||
assert_eq!(two, vec!["alpha beta", "gamma delta"]);
|
||||
// > max rows: clamp to max, last row ends with an ellipsis,
|
||||
// and every row stays within the width.
|
||||
let many = clamp_wrapped(
|
||||
"alpha beta gamma delta epsilon zeta eta theta iota",
|
||||
11,
|
||||
3,
|
||||
);
|
||||
assert_eq!(many.len(), 3);
|
||||
assert!(many[2].ends_with('…'), "last row ellipsized: {many:?}");
|
||||
for row in &many {
|
||||
assert!(row.chars().count() <= 11, "row within width: {row:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dark_theme_default_view_snapshot() {
|
||||
let mut app = App::new();
|
||||
|
||||
Reference in New Issue
Block a user