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
|
extended theme mapping/contrast tests. Text snapshots are colour-blind
|
||||||
(`render_to_string` strips style), so none churned.
|
(`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
|
## Out of scope
|
||||||
|
|
||||||
Deliberately deferred to keep this ADR shippable as a single
|
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-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-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-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-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-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`)
|
- [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.
|
# placeholders. ADR-0009's surface conventions apply.
|
||||||
usage:
|
usage:
|
||||||
create_table: "create table <Name> with pk [<col>(<type>)[, ...]]"
|
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_drop_table: "drop table [if exists] <Name>"
|
||||||
sql_create_index: "create [unique] index [if not exists] [<Name>] on <Table> (<col>[, ...])"
|
sql_create_index: "create [unique] index [if not exists] [<Name>] on <Table> (<col>[, ...])"
|
||||||
sql_drop_index: "drop index [if exists] <Name>"
|
sql_drop_index: "drop index [if exists] <Name>"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1540
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -16,13 +17,13 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ ADVANCED ────────────────────────────────────────╮
|
│ │╭ ADVANCED ────────────────────────────────────────╮
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for│
|
│ ││Type a command — press Tab for options, `help` │
|
||||||
|
│ ││for a list │
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · mode simple switch · Ctrl-C quit
|
Enter submit · mode simple switch · Ctrl-C quit
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1523
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -16,13 +17,13 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for│
|
│ ││Type a command — press Tab for options, `help` │
|
||||||
|
│ ││for a list │
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1531
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -16,13 +17,13 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for│
|
│ ││Type a command — press Tab for options, `help` │
|
||||||
|
│ ││for a list │
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1583
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -15,14 +16,14 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
|
||||||
│ ││ │
|
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││insert into T values (1, 'hi', null) --all-r │
|
│ ││insert into T values (1, 'hi', null) --all-r │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││after `insert into T values (1, 'hi', null)`, │
|
│ ││after `insert into T values (1, 'hi', null)`, │
|
||||||
|
│ ││expected end of input — usage: insert into │
|
||||||
|
│ ││<Table> [(<col>[, ...])] [values] (<value>[, ...])│
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 1771
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -16,13 +17,13 @@ expression: snapshot
|
|||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for│
|
│ ││Type a command — press Tab for options, `help` │
|
||||||
|
│ ││for a list │
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 1469
|
assertion_line: 1613
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ──────────────────────────────────────────╮
|
||||||
@@ -17,13 +17,13 @@ expression: snapshot
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │Continue? │ │
|
│ │Continue? │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │[Y] Yes [N] No Esc cancel │ │
|
│ │[Y] Yes [N] No Esc cancel │─────────╯
|
||||||
│ ╰──────────────────────────────────────────────────────────╯─────────╯
|
│ ╰──────────────────────────────────────────────────────────╯─────────╮
|
||||||
│ │╭ SIMPLE ──────────────────────────────────────────╮
|
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ │╰──────────────────────────────────────────────────╯
|
│ │╰──────────────────────────────────────────────────╯
|
||||||
│ │╭ Hint ────────────────────────────────────────────╮
|
│ │╭ Hint ────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for│
|
│ ││Type a command — press Tab for options, `help` │
|
||||||
|
│ ││for a list │
|
||||||
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰──────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
||||||
|
|||||||
@@ -493,6 +493,36 @@ fn wrap_lines(s: &str, width: usize) -> Vec<String> {
|
|||||||
lines
|
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) {
|
fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||||
let label_style = Style::default().fg(theme.muted);
|
let label_style = Style::default().fg(theme.muted);
|
||||||
let value_style = Style::default()
|
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) {
|
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()
|
let rows = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Min(5), // Output panel
|
Constraint::Min(5), // Output panel
|
||||||
Constraint::Length(3), // Input panel
|
Constraint::Length(3), // Input panel
|
||||||
Constraint::Length(3), // Hint panel
|
Constraint::Length(hint_content), // Hint panel (dynamic)
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
render_output_panel(app, theme, frame, rows[0]);
|
render_output_panel(app, theme, frame, rows[0]);
|
||||||
render_input_panel(app, theme, frame, rows[1]);
|
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) {
|
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)
|
(effective, effective_cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
/// Resolve the Hint panel body into its rendered lines, pre-wrapped
|
||||||
let block = Block::default()
|
/// to the panel's inner width and clamped to `MAX_HINT_ROWS` with an
|
||||||
.borders(Borders::ALL)
|
/// ellipsis backstop (issue #12). The returned line count is the
|
||||||
.border_type(BorderType::Rounded)
|
/// content-row count `render_right_column` allocates for, so the
|
||||||
.border_style(Style::default().fg(theme.border))
|
/// panel grows for a long hint and reclaims the space for a short
|
||||||
.title(Span::styled(
|
/// one.
|
||||||
format!(" {} ", crate::t!("panel.hint_title")),
|
///
|
||||||
Style::default()
|
/// Resolution order for the body:
|
||||||
.fg(theme.fg)
|
/// 1. An explicit app-set hint (e.g. modal contexts) wins.
|
||||||
.add_modifier(Modifier::BOLD),
|
/// 2. Otherwise, with non-empty input, the ambient
|
||||||
))
|
/// typing-assistance hint (ADR-0022 §6) in the effective mode.
|
||||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
/// 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
|
// In one-shot advanced mode (`:` prefix in simple mode) the
|
||||||
// raw input carries the `:` sigil, which is not part of the
|
// raw input carries the `:` sigil, which is not part of the
|
||||||
// grammar. Strip it for the ambient computation so the hint
|
// 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.schema_cache,
|
||||||
app.effective_mode().as_mode(),
|
app.effective_mode().as_mode(),
|
||||||
);
|
);
|
||||||
let muted = Style::default().fg(theme.muted);
|
match (app.hint.as_deref(), ambient) {
|
||||||
let line = match (app.hint.as_deref(), ambient) {
|
(Some(set), _) => prose(set),
|
||||||
(Some(set), _) => Line::from(Span::styled(set.to_string(), muted)),
|
(None, Some(crate::input_render::AmbientHint::Prose(text))) => prose(&text),
|
||||||
(None, Some(crate::input_render::AmbientHint::Prose(text))) => {
|
|
||||||
Line::from(Span::styled(text, muted))
|
|
||||||
}
|
|
||||||
(None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => {
|
(None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => {
|
||||||
// ADR-0022 §7 + user's #2: render items with the
|
vec![render_candidate_line(&items, selected, inner, theme)]
|
||||||
// selected one highlighted; if not all fit,
|
}
|
||||||
// scroll horizontally with `<` / `>` markers at
|
(None, None) => prose(&crate::t!("panel.hint_empty")),
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
(None, None) => Line::from(Span::styled(empty_hint, muted)),
|
|
||||||
};
|
|
||||||
let paragraph = Paragraph::new(line)
|
|
||||||
.block(block)
|
|
||||||
.wrap(Wrap { trim: false });
|
|
||||||
|
|
||||||
|
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);
|
frame.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1371,6 +1429,93 @@ mod tests {
|
|||||||
out
|
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]
|
#[test]
|
||||||
fn dark_theme_default_view_snapshot() {
|
fn dark_theme_default_view_snapshot() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user