DSL parser, async DB worker, types, history, metadata, polish
Track 1 implementation plus polish round. Parser (chumsky): - Grammar-based DSL producing a typed Command AST. - create table X with pk [name:type[,name:type...]] supports arbitrary names, any user type, compound PKs natively. Bare form errors with a friendly hint pointing at `with pk`. - add column to table X: Name (type); drop table X. - Required clauses use keyword grammar; -- reserved for opt-in flags (ADR-0009). Custom Rich reasons preferred when surfacing chumsky errors so unknown-type messages list valid alternatives. Database (ADR-0010, ADR-0012): - rusqlite + STRICT tables + foreign_keys=ON. - Dedicated worker thread; mpsc Request inbox, oneshot replies. - Typed DbError with friendly_message() hook for H1. - Internal __rdbms_playground_columns metadata table preserves user-facing types across schema reads, atomically maintained alongside DDL via Connection transactions. list_tables hides it via the new __rdbms_ internal-table convention. Types (ADR-0005, ADR-0011): - All ten user-facing types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid. - Type::fk_target_type() for FK-side column-type rule (Serial->Int, ShortId->Text, others identity) -- foundation for the FK iteration. App / Runtime / UI: - update() stays pure-sync; runtime dispatches DSL via spawned tasks, results post back as AppEvent::Dsl*. - Items panel renders live tables list; output panel shows the user-facing structure of the current table after each DDL. - In-memory command history (Up/Down, draft preservation, consecutive-duplicate dedup) -- I2 partial. - Mouse capture removed; terminal native text selection restored (toggle approach revisited when scroll/click features land). Docs: - ADRs 0009 (DSL syntax conventions), 0010 (DB worker), 0011 (FK type compat), 0012 (internal metadata table). - requirements.md progress notes; new V4 entry for the scrollable session-log + inline rich rendering + Markdown export direction. Tests: 103 passing (91 lib + 12 integration), 0 skipped. Clippy clean with nursery enabled.
This commit is contained in:
@@ -32,7 +32,7 @@ pub fn render(app: &App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
.constraints([Constraint::Length(28), Constraint::Min(20)])
|
||||
.split(outer[0]);
|
||||
|
||||
render_items_panel(theme, frame, columns[0]);
|
||||
render_items_panel(app, theme, frame, columns[0]);
|
||||
render_right_column(app, theme, frame, columns[1]);
|
||||
render_status_bar(app, theme, frame, outer[1]);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ fn paint_background(theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
|
||||
fn render_items_panel(theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
fn render_items_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
@@ -70,13 +70,39 @@ fn render_items_panel(theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
))
|
||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||
|
||||
let placeholder = Paragraph::new(Line::from(Span::styled(
|
||||
"(none yet)",
|
||||
Style::default().fg(theme.muted).add_modifier(Modifier::ITALIC),
|
||||
)))
|
||||
.block(block);
|
||||
if app.tables.is_empty() {
|
||||
let placeholder = Paragraph::new(Line::from(Span::styled(
|
||||
"(none yet)",
|
||||
Style::default()
|
||||
.fg(theme.muted)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)))
|
||||
.block(block);
|
||||
frame.render_widget(placeholder, area);
|
||||
return;
|
||||
}
|
||||
|
||||
frame.render_widget(placeholder, area);
|
||||
let highlight = app
|
||||
.current_table
|
||||
.as_ref()
|
||||
.map(|t| t.name.as_str())
|
||||
.unwrap_or_default();
|
||||
let lines: Vec<Line<'_>> = app
|
||||
.tables
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let style = if name == highlight {
|
||||
Style::default()
|
||||
.fg(theme.fg)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(theme.fg)
|
||||
};
|
||||
Line::from(Span::styled(name.as_str(), style))
|
||||
})
|
||||
.collect();
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_output_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
@@ -300,4 +326,61 @@ mod tests {
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("one_shot_advanced_dark", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn populated_with_table_snapshot() {
|
||||
// Items panel lists tables; output panel shows the
|
||||
// structure of the current table.
|
||||
use crate::app::{OutputKind, OutputLine};
|
||||
use crate::db::{ColumnDescription, TableDescription};
|
||||
|
||||
let mut app = App::new();
|
||||
app.tables = vec!["Customers".to_string(), "Orders".to_string()];
|
||||
use crate::dsl::Type;
|
||||
let desc = TableDescription {
|
||||
name: "Customers".to_string(),
|
||||
columns: vec![
|
||||
ColumnDescription {
|
||||
name: "id".to_string(),
|
||||
user_type: Some(Type::Serial),
|
||||
sqlite_type: "INTEGER".to_string(),
|
||||
notnull: false,
|
||||
primary_key: true,
|
||||
},
|
||||
ColumnDescription {
|
||||
name: "Name".to_string(),
|
||||
user_type: Some(Type::Text),
|
||||
sqlite_type: "TEXT".to_string(),
|
||||
notnull: false,
|
||||
primary_key: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
app.current_table = Some(desc);
|
||||
// Mirror what the App writes when a DSL command succeeds.
|
||||
app.output.push_back(OutputLine {
|
||||
text: "[ok] create table Customers".to_string(),
|
||||
kind: OutputKind::System,
|
||||
mode_at_submission: Mode::Simple,
|
||||
});
|
||||
app.output.push_back(OutputLine {
|
||||
text: " Customers".to_string(),
|
||||
kind: OutputKind::System,
|
||||
mode_at_submission: Mode::Simple,
|
||||
});
|
||||
app.output.push_back(OutputLine {
|
||||
text: " id serial [PK]".to_string(),
|
||||
kind: OutputKind::System,
|
||||
mode_at_submission: Mode::Simple,
|
||||
});
|
||||
app.output.push_back(OutputLine {
|
||||
text: " Name text".to_string(),
|
||||
kind: OutputKind::System,
|
||||
mode_at_submission: Mode::Simple,
|
||||
});
|
||||
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("populated_with_table_dark", snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user