diff --git a/Cargo.lock b/Cargo.lock index 05d5f5a..fe26eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,6 +607,8 @@ name = "tb-cli" version = "0.1.0" dependencies = [ "anyhow", + "crossterm", + "ratatui", "tb-frontend", "tb-ide", "tb-runtime", diff --git a/PLAN.md b/PLAN.md index 9df7266..e0dabf6 100644 --- a/PLAN.md +++ b/PLAN.md @@ -446,15 +446,15 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. virtuelle Ereignisfolge reproduzierbar ein. ## Phase 5 — IDE (`tb-ide`) -- [ ] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile, +- [x] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile, klassisches Farbschema -- [ ] Editor: Syntaxprüfung/-normalisierung pro Zeile (Keywords groß, +- [x] Editor: Syntaxprüfung/-normalisierung pro Zeile (Keywords groß, Leerzeichen), Suchen/Ersetzen, Hilfe-Verweise -- [ ] Projektverwaltung (`.MAK`): mehrere Module/Formulare -- [ ] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren, - Eigenschaftenfenster -- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart -- [ ] **Export-Bedienung der IDE:** Run → Make EXE File und Make Library +- [x] Projektverwaltung (`.MAK`): mehrere Module/Formulare +- [x] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren, + Properties Bar +- [x] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart +- [x] **Export-Bedienung der IDE:** Run → Make EXE File und Make Library mit vollständigen Dialogen, Zielsystem/-architektur, Ausgabepfad, Eingabeprüfung, Status-/Fehleranzeige und Übergabe an den Exportpfad vorbereiten. Die UI gehört vollständig zu Phase 5. Solange der @@ -469,16 +469,23 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste. - [x] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten. Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben. **1 Inventareintrag** -- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster, +- [x] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster, Überwachungsausdrücke -- [ ] Hilfe-System: rendert die Markdown-Dokumentation aus `docs/` +- [x] Hilfe-System: rendert die Markdown-Dokumentation aus `docs/` (Sprach- und Bibliotheksreferenz) im Help-Fenster — Konsolen- Markdown-Rendering mit Umbruch nach aktueller Fenstergröße, Hyperlink-Navigation wie im Vorbild (Tab/Enter/Alt+F1), F1 = kontextsensitiver Sprung zum Thema unter dem Cursor -- [ ] Meilenstein: Programm komplett in der IDE schreiben, gestalten, +- [x] Meilenstein: Programm komplett in der IDE schreiben, gestalten, debuggen und ausführen +Abgenommen am 2026-09-07 durch Change `phase-5-08-integration-und-phasenabnahme`. +Die zugehörigen `verification.md` und `befehlsmatrix.md` im OpenSpec-Change +(bei Abschluss unter `openspec/changes/archive/`) ordnen alle elf Planpunkte +und 45 Referenzbereiche den bestandenen Tests zu. Workspace-Regressionen, +headless IDE/CLI/TBC-Parität, lokaler Unix-PTY-Test und Release-Compile-Budget +sind bestanden. Die vollständige Terminal-/OS-Matrix bleibt Phase 6. + ## Phase 6 — Kompatibilität, Politur, Distribution - [ ] Kompatibilitäts-Testsuite ausbauen (Snapshot-Tests der Bildschirmausgabe) - [ ] Plattformtests: Windows Terminal, Linux (mind. 2 Emulatoren), macOS — diff --git a/crates/tb-cli/Cargo.toml b/crates/tb-cli/Cargo.toml index 765f65e..35d239a 100644 --- a/crates/tb-cli/Cargo.toml +++ b/crates/tb-cli/Cargo.toml @@ -19,3 +19,5 @@ anyhow.workspace = true [dev-dependencies] tb-ide = { path = "../tb-ide" } +crossterm.workspace = true +ratatui.workspace = true diff --git a/crates/tb-cli/src/ide_acceptance.rs b/crates/tb-cli/src/ide_acceptance.rs new file mode 100644 index 0000000..0d0897a --- /dev/null +++ b/crates/tb-cli/src/ide_acceptance.rs @@ -0,0 +1,804 @@ +//! Phase-5 acceptance drives App through native input; CLI comparisons use run_chain itself. +use super::*; +use crossterm::event::{ + Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind, +}; +use ratatui::{backend::TestBackend, Terminal}; +use std::{ + fs, + sync::atomic::{AtomicUsize, Ordering}, +}; +use tb_ide::{ + app::{App, DialogKind, Execution, Hit, Mode}, + commands::Command, + export::{ExportStatus, ProjectStamp}, +}; +use tb_runtime::{snapshot, value::Value}; +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + static N: AtomicUsize = AtomicUsize::new(0); + let p = std::env::temp_dir().join(format!( + "tb-accept-{}-{}", + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&p).unwrap(); + Self(p.canonicalize().unwrap()) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} +fn key(a: &mut App, k: K, m: M) { + a.handle(Event::Key(KeyEvent::new(k, m))); +} +fn plain(a: &mut App, k: K) { + key(a, k, M::NONE); +} +fn text(a: &mut App, s: &str) { + for c in s.chars() { + plain(a, K::Char(c)); + } +} +fn field(a: &mut App, n: usize, s: &str) { + for _ in 0..30 { + if a.dialog.as_ref().unwrap().focus == n { + break; + } + plain(a, K::Tab); + } + assert_eq!(a.dialog.as_ref().unwrap().focus, n); + key(a, K::Char('a'), M::CONTROL); + plain(a, K::Delete); + text(a, s); +} +fn choice(a: &mut App, n: usize, s: &str) { + for _ in 0..30 { + if a.dialog.as_ref().unwrap().focus == n { + break; + } + plain(a, K::Tab); + } + for _ in 0..200 { + if a.dialog.as_ref().unwrap().fields[n] + .string() + .eq_ignore_ascii_case(s) + { + return; + } + plain(a, K::Right); + } + panic!("choice missing {s}: {:?}", a.dialog); +} +fn submit(a: &mut App) { + plain(a, K::Enter); + assert!(a.dialog.is_none(), "{:?} / {}", a.dialog, a.message); +} +fn menu(a: &mut App, c: Command) { + let menus = a.menus(); + let (m, i) = menus + .iter() + .enumerate() + .find_map(|(m, menu)| { + menu.items + .iter() + .position(|i| i.command == Some(c)) + .map(|i| (m, i)) + }) + .unwrap_or_else(|| panic!("menu missing {c:?}")); + key(a, K::Char(menus[m].mnemonic), M::ALT); + for _ in 0..i { + plain(a, K::Down); + } + plain(a, K::Enter); + assert_eq!(a.last_command, Some(c), "{}", a.message); +} +fn draw(a: &mut App) -> String { + let mut t = Terminal::new(TestBackend::new(a.size.0, a.size.1)).unwrap(); + t.draw(|f| a.render(f)).unwrap(); + t.backend() + .buffer() + .content + .chunks(a.size.0 as usize) + .map(|r| r.iter().map(|c| c.symbol()).collect::()) + .collect::>() + .join("\n") +} +fn click_hit(a: &mut App, predicate: impl Fn(&Hit) -> bool) { + draw(a); + let r = a + .hits + .iter() + .rev() + .find(|(_, h)| predicate(h)) + .unwrap_or_else(|| panic!("hit missing")) + .0; + a.handle(Event::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: r.x, + row: r.y, + modifiers: M::NONE, + })); +} +fn replace_code(a: &mut App, s: &str) { + key(a, K::Char('a'), M::CONTROL); + a.handle(Event::Paste(s.into())); +} +fn code(a: &App) -> String { + a.project + .document(a.active_document().unwrap()) + .unwrap() + .code() + .into() +} +fn cursor_line(a: &mut App, line: usize) { + key(a, K::Home, M::CONTROL); + for _ in 1..line { + plain(a, K::Down); + } + plain(a, K::Home); +} +fn save_as(a: &mut App, path: &Path) { + menu(a, Command::SaveAs); + field(a, 0, &path.display().to_string()); + submit(a); +} +fn property(a: &mut App, name: &str, value: &str) { + if !a.properties { + plain(a, K::F(10)); + } + click_hit(a, |h| matches!(h, Hit::DesignAction("property"))); + choice(a, 0, name); + submit(a); + plain(a, K::F(2)); + key(a, K::Char('a'), M::CONTROL); + text(a, value); + plain(a, K::Enter); + assert!(a.dialog.is_none(), "{:?}", a.dialog); + assert!(!a.value_focus); +} +fn tick(a: &mut App, now: u64) { + for _ in 0..40 { + a.tick_execution(now); + if !matches!(a.execution, Execution::Running) { + break; + } + } +} +fn open_code(a: &mut App, name: &str) { + menu(a, Command::Code); + if a.dialog.is_none() { + menu(a, Command::Code); + } + choice(a, 0, name); + submit(a); + assert_eq!(a.mode, Mode::Environment); +} +fn create(a: &mut App, t: &Temp) -> PathBuf { + assert_eq!(a.project.members().len(), 1); + assert!(code(a).is_empty()); + assert!(draw(a).contains("Untitled")); + menu(a, Command::NewProject); + if a.dialog.is_some() { + choice(a, 0, "Verwerfen"); + submit(a); + } + assert!(code(a).is_empty()); + menu(a, Command::SyntaxChecking); + let include=format!("total=1\nCALL Bump(total)\nForm1.Hide\nCLS\nLINE INPUT input$\nPRINT total; input$; COMMAND$\nOPEN \"{}\" FOR OUTPUT AS #1\nPRINT #1, total\nCLOSE #1\nIF COMMAND$=\"error\" THEN ERROR 5\nEND\n",t.0.join("result.txt").display()); + replace_code(a, &include); + menu(a, Command::SaveText); + field(a, 0, &t.0.join("work.bi").display().to_string()); + submit(a); + replace_code(a, "' Main module\n"); + save_as(a, &t.0.join("main.bas")); + menu(a, Command::NewModule); + field(a, 0, "Helper"); + submit(a); + replace_code(a, "SUB Bump(n AS INTEGER)\nn=n+1\nEND SUB\n"); + save_as(a, &t.0.join("helper.bas")); + menu(a, Command::NewForm); + assert_eq!(a.mode, Mode::Designer); + property(a, "CAPTION", "Abnahme"); + let designer_focus = a.active; + plain(a, K::F(1)); + assert_eq!(a.help.page().path, "docs/forms-referenz.md"); + plain(a, K::Tab); + plain(a, K::Enter); + key(a, K::F(1), M::ALT); + plain(a, K::Esc); + assert_eq!(a.active, designer_focus); + plain(a, K::F(12)); + plain(a, K::Enter); + choice(a, 0, "Load"); + submit(a); + let handler = code(a); + assert!(handler.to_uppercase().contains("FORM_LOAD"), "{handler}"); + cursor_line(a, 1); + key(a, K::End, M::CONTROL); // Replace body through the editor, retaining the generated event signature. + replace_code( + a, + &format!( + "DECLARE SUB Bump(n AS INTEGER)\nDIM SHARED total AS INTEGER\n{}", + handler.replace("END SUB", "SHARED total\n'$INCLUDE: 'work.bi'\nEND SUB") + ), + ); + key(a, K::F(12), M::SHIFT); + assert_eq!(a.mode, Mode::Designer); + // Toolbox Enter is the documented keyboard equivalent of double-click placement. + menu(a, Command::Toolbox); + for _ in 0..2 { + plain(a, K::Down); + } + plain(a, K::Enter); + assert!(a.designer.selections.values().any(|s| !s.is_empty())); + property(a, "INDEX", "0"); + property(a, "CAPTION", "Start"); + menu(a, Command::Toolbox); + plain(a, K::Down); + plain(a, K::Down); + plain(a, K::Enter); + property(a, "INDEX", "1"); + property(a, "CTLNAME", "CommandButton1"); + menu(a, Command::MenuDesign); + plain(a, K::Insert); + plain(a, K::Enter); + field(a, 1, "&Datei"); + submit(a); + // Save all members and the chosen startup through actual dialog fields. + open_code(a, "main.bas"); + menu(a, Command::Startup); + choice(a, 0, "Form1.frm"); + submit(a); + let path = t.0.join("project.mak"); + menu(a, Command::SaveProject); + field(a, 0, &path.display().to_string()); + submit(a); + assert_eq!(a.project.members().len(), 3); + assert!(!a.project.is_dirty()); + let form = fs::read_to_string(t.0.join("Form1.frm")).unwrap(); + let parsed = tb_ui::frm::read_text("Form1.frm", &form).unwrap(); + let array: Vec<_> = parsed + .root + .children + .iter() + .filter(|n| n.class == tb_frontend::forms::ObjectClass::CommandButton) + .map(tb_ide::designer::node_key) + .collect(); + assert_eq!( + array, + vec![ + ("COMMANDBUTTON1".into(), Some(0)), + ("COMMANDBUTTON1".into(), Some(1)) + ] + ); + assert!(form.to_uppercase().contains("BEGIN MENU")); + assert!(form.to_uppercase().contains("FORM_LOAD")); + path +} +fn exports(a: &mut App, t: &Temp) { + let stamp = ProjectStamp::capture(&a.project); + for c in [Command::MakeExe, Command::MakeLibrary] { + menu(a, c); + assert!(draw(a).contains("Erzeugen: Phase 6")); + choice(a, 1, "linux"); + choice(a, 2, "x86_64"); + field(a, 3, ""); + plain(a, K::Enter); + assert!(a.dialog.as_ref().unwrap().error.contains("Ausgabepfad")); + field(a, 3, &t.0.join("bad.tbc").display().to_string()); + plain(a, K::Enter); + assert!(a.dialog.as_ref().unwrap().error.contains("TBC")); + let existing = t.0.join("existing"); + fs::write(&existing, "original").unwrap(); + field(a, 3, &existing.display().to_string()); + plain(a, K::Enter); + assert!(a + .dialog + .as_ref() + .unwrap() + .error + .contains("Überschreibentscheidung")); + plain(a, K::Tab); + plain(a, K::Char(' ')); + plain(a, K::Enter); + assert_eq!(fs::read_to_string(existing).unwrap(), "original"); + let out = t.0.join("native-result"); + field(a, 3, &out.display().to_string()); + plain(a, K::Enter); + let request = a.dialog.as_ref().unwrap().request.clone().unwrap(); + assert_eq!(request.project, stamp); + for status in [ + ExportStatus::Running, + ExportStatus::Failed("Kontrollierter Fehler".into()), + ExportStatus::Cancelled, + ExportStatus::Success, + ] { + a.export_result(&request, status.clone()).unwrap(); + assert!(draw(a).contains(&status.text())); + assert!(!out.exists()); + } + plain(a, K::Esc); + assert_eq!(ProjectStamp::capture(&a.project), stamp); + } +} +#[test] +fn create_save_debug_help_exports_and_reopen() { + let t = Temp::new(); + let mut a = App::new(&t.0, t.0.join("options"), (80, 25)).unwrap(); + let path = create(&mut a, &t); + let saved: Vec<_> = [ + "main.bas", + "helper.bas", + "Form1.frm", + "work.bi", + "project.mak", + ] + .map(|p| fs::read(t.0.join(p)).unwrap()) + .into(); + exports(&mut a, &t); + open_code(&mut a, "Form1.frm"); + let line = code(&a) + .lines() + .position(|l| l.contains("$INCLUDE")) + .unwrap() + + 1; + cursor_line(&mut a, line); + menu(&mut a, Command::IncludedFile); + assert!(a + .project + .document(a.active_document().unwrap()) + .unwrap() + .source_path() + .ends_with("work.bi")); + cursor_line(&mut a, 2); + plain(&mut a, K::F(9)); + let code_focus = a.active; + cursor_line(&mut a, 6); + plain(&mut a, K::F(1)); + assert!(a.help.page().title.contains("Index") || a.help.page().path.starts_with("docs/")); + plain(&mut a, K::Tab); + plain(&mut a, K::Enter); + key(&mut a, K::F(1), M::ALT); + plain(&mut a, K::Esc); + assert_eq!(a.active, code_focus); + menu(&mut a, Command::CommandLine); + field(&mut a, 0, "!"); + submit(&mut a); + key(&mut a, K::F(5), M::SHIFT); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Paused, "{}", a.message); + assert!(a + .session + .vm + .as_ref() + .unwrap() + .current_file() + .ends_with("work.bi")); + assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 2); + plain(&mut a, K::F(10)); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Paused); + assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3); + menu(&mut a, Command::AddWatch); + field(&mut a, 0, "total"); + submit(&mut a); + assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(2)))); + menu(&mut a, Command::Immediate); + text(&mut a, "total=9"); + plain(&mut a, K::Enter); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Paused); + plain(&mut a, K::F(5)); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Waiting, "{}", a.message); + menu(&mut a, Command::Output); + a.handle(Event::Paste("abc\n".into())); + tick(&mut a, 100); + assert_eq!(a.execution, Execution::Ended, "{}", a.message); + assert_eq!(snapshot::text(a.session.screen()), "? abc\n 9 abc!\n"); + assert!(fs::read_to_string(t.0.join("result.txt")) + .unwrap() + .contains('9')); + for (p, bytes) in [ + "main.bas", + "helper.bas", + "Form1.frm", + "work.bi", + "project.mak", + ] + .iter() + .zip(saved.iter()) + { + assert_eq!(&fs::read(t.0.join(p)).unwrap(), bytes); + } + menu(&mut a, Command::NewProject); + if a.dialog.is_some() { + choice(&mut a, 0, "Verwerfen"); + submit(&mut a); + } + menu(&mut a, Command::OpenProject); + field(&mut a, 0, &path.display().to_string()); + plain(&mut a, K::Enter); + choice(&mut a, 0, "Verwerfen"); + submit(&mut a); + assert_eq!(a.project.members().len(), 3); + assert!(a + .project + .document(a.project.startup().unwrap()) + .unwrap() + .source_path() + .ends_with("Form1.frm")); + assert!(!a.project.is_dirty()); + for id in a.project.members() { + let doc = a.project.document(id).unwrap(); + match doc.content() { + tb_vm::project_io::Content::Form(form) => assert_eq!( + form.as_ref(), + &tb_ui::frm::read_text( + &doc.source_path().display().to_string(), + &fs::read_to_string(doc.source_path()).unwrap() + ) + .unwrap() + ), + _ => assert_eq!(doc.code(), fs::read_to_string(doc.source_path()).unwrap()), + } + } + menu(&mut a, Command::CommandLine); + field(&mut a, 0, "!"); + submit(&mut a); + menu(&mut a, Command::Start); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Waiting); + menu(&mut a, Command::Output); + a.handle(Event::Paste("abc\n".into())); + tick(&mut a, 100); + assert_eq!(a.execution, Execution::Ended); + assert_eq!(snapshot::text(a.session.screen()), "? abc\n 2 abc!\n"); +} + +struct ScriptHost { + now: u64, + sent: bool, + events: std::collections::VecDeque, +} +impl Host for ScriptHost { + fn present(&mut self, _: &tb_runtime::screen::TextScreen) {} + fn jetzt_ms(&mut self) -> u64 { + self.now + } + fn next_event(&mut self, _: bool) -> Option { + self.events.pop_front() + } + fn warten(&mut self, _: Option) -> Option { + assert!(!self.sent, "Unexpected extra wait"); + self.sent = true; + self.now = 100; + self.events + .extend("abc\r".chars().map(|c| Ereignis::Taste(c.to_string(), 0))); + self.events.pop_front() + } +} +#[test] +fn saved_project_ide_source_cli_and_tbc_have_identical_sessions() { + let t = Temp::new(); + let mut a = App::new(&t.0, t.0.join("options"), (80, 25)).unwrap(); + let path = create(&mut a, &t); + assert_eq!( + super::cmd_build(&[path.display().to_string()]), + ExitCode::SUCCESS + ); + for command in ["!", "error"] { + menu(&mut a, Command::CommandLine); + field(&mut a, 0, command); + submit(&mut a); + fs::write(t.0.join("result.txt"), "initial").unwrap(); + menu(&mut a, Command::Start); + let ide = a.session.vm.as_ref().unwrap(); + assert_eq!(ide.rt.command, command); + assert_eq!((ide.rt.screen.cols(), ide.rt.screen.rows()), (80, 25)); + assert!(ide.forms.has_visible_forms()); + assert!(!ide.rt.dateien.ist_offen(1)); + assert!(a.session.host.events.is_empty()); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Waiting, "{}", a.message); + // Same input and time boundary as ScriptHost::warten, through native App input. + a.handle(Event::Paste("abc\n".into())); + tick(&mut a, 100); + assert_eq!( + a.execution, + if command == "!" { + Execution::Ended + } else { + Execution::Error + }, + "{}", + a.message + ); + if command == "error" { + assert!(a.message.contains('5')); + } + let output = snapshot::text(a.session.screen()); + let file = fs::read(t.0.join("result.txt")).unwrap(); + assert_eq!(output, format!("? abc\n 2 abc{command}\n")); + assert!(String::from_utf8_lossy(&file).contains('2')); + for target in [path.clone(), path.with_extension("tbc")] { + fs::write(t.0.join("result.txt"), "initial").unwrap(); + let input = super::compile(Some(&target.display().to_string())) + .unwrap() + .1; + let initial = new_execution(input, command, Some((80, 25)), None).unwrap(); + assert!(initial.forms.has_visible_forms()); + assert_eq!(initial.rt.command, command); + assert!(!initial.rt.dateien.ist_offen(1)); + let mut host = ScriptHost { + now: 0, + sent: false, + events: Default::default(), + }; + let (event, vm) = super::run_chain( + &[target.display().to_string(), command.into()], + &mut host, + Some((80, 25)), + ) + .unwrap(); + if command == "!" { + assert_eq!(event, RunEvent::Ended); + } else { + assert!( + matches!(event, RunEvent::Error { code: 5, .. }), + "{event:?}" + ); + } + assert_eq!(snapshot::text(&vm.rt.screen), output); + assert_eq!(fs::read(t.0.join("result.txt")).unwrap(), file); + assert!(!vm.rt.dateien.ist_offen(1)); + assert_eq!(host.now, 100); + } + menu(&mut a, Command::Restart); + assert!(a.session.host.events.is_empty()); + assert!(!a.session.vm.as_ref().unwrap().rt.dateien.ist_offen(1)); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Waiting); + a.handle(Event::Paste("abc\n".into())); + tick(&mut a, 100); + assert_eq!(snapshot::text(a.session.screen()), output); + } +} + +#[test] +fn every_menu_command_has_native_keyboard_and_mouse_routing() { + let t = Temp::new(); + for designer in [false, true] { + let menus = tb_ide::commands::menus(designer); + for (m, group) in menus.iter().enumerate() { + for (i, item) in group.items.iter().enumerate() { + let Some(command) = item.command else { + continue; + }; + for mouse in [false, true] { + let mut a = App::new(&t.0, t.0.join("options"), (120, 45)).unwrap(); + if designer { + menu(&mut a, Command::NewForm); + } + if mouse { + if designer { + plain(&mut a, K::F(11)); + plain(&mut a, K::Esc); + } + click_hit(&mut a, |h| matches!(h,Hit::Menu(n) if *n==m)); + click_hit(&mut a, |h| matches!(h,Hit::MenuItem(n) if *n==i)); + assert_eq!(a.last_command, Some(command), "{command:?}"); + } else { + menu(&mut a, command); + } + assert!(a.basic_events.is_empty(), "{command:?}"); + assert!(!a.message.contains("folgt in Phase-5")); + } + } + } + } +} + +#[test] +fn function_keys_conflict_contexts_and_errors_keep_a_single_input_owner() { + let t = Temp::new(); + let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap(); + menu(&mut a, Command::SyntaxChecking); + replace_code(&mut a, "PRINT (\n"); + menu(&mut a, Command::Start); + assert_eq!(a.execution, Execution::Error); + assert!(a.session.vm.is_none()); + let invalid = code(&a); + assert!(invalid.contains("PRINT (")); + replace_code(&mut a, "LINE INPUT s$\nPRINT s$\nEND\n"); + plain(&mut a, K::F(5)); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Waiting); + plain(&mut a, K::F(4)); + assert!(a.session.fullscreen); + plain(&mut a, K::F(4)); + assert!(!a.session.fullscreen); + plain(&mut a, K::F(11)); + assert!(a.menu.is_some()); + plain(&mut a, K::Esc); + plain(&mut a, K::F(2)); + assert!(matches!( + a.dialog.as_ref().unwrap().kind, + DialogKind::ChooseCode + )); + plain(&mut a, K::Esc); + plain(&mut a, K::F(12)); + assert!(a.basic_events.is_empty()); + if a.dialog.is_some() { + plain(&mut a, K::Esc); + } + key(&mut a, K::Pause, M::CONTROL); + assert_eq!(a.execution, Execution::Paused); + assert!(a.basic_events.is_empty()); + a.handle(Event::Resize(40, 10)); + assert!(draw(&mut a).contains("80×25")); + a.handle(Event::Resize(100, 30)); + assert!(draw(&mut a).contains("Untitled")); + plain(&mut a, K::F(5)); + menu(&mut a, Command::Output); + text(&mut a, "one"); + plain(&mut a, K::Enter); + tick(&mut a, 100); + assert_eq!(snapshot::text(a.session.screen()), "? one\none\n"); + open_code(&mut a, "Untitled.bas"); + replace_code(&mut a, "RUN \"missing\"\n"); + menu(&mut a, Command::Start); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Error); + assert!(a.message.contains("nicht gefunden")); + open_code(&mut a, "Untitled.bas"); + assert!(code(&a).contains("missing")); + replace_code(&mut a, "PRINT SHELL(\"exit 7\")\nEND\n"); + menu(&mut a, Command::Start); + tick(&mut a, 0); + assert_eq!( + a.session.host.shell_request.take().as_deref(), + Some("exit 7") + ); + a.session.host.shell_result = Some(Ok(7)); + tick(&mut a, 0); + assert_eq!(snapshot::text(a.session.screen()), " 7 \n"); + menu(&mut a, Command::NewForm); + plain(&mut a, K::F(2)); + assert!(a.value_focus); + plain(&mut a, K::Esc); + plain(&mut a, K::F(10)); + assert!(!a.properties && a.menu.is_some()); + plain(&mut a, K::F(10)); + assert!(a.properties && a.menu.is_none()); + plain(&mut a, K::F(11)); + assert!(a.menu.is_some()); + plain(&mut a, K::Esc); + plain(&mut a, K::F(12)); + assert!(matches!( + a.dialog.as_ref().unwrap().kind, + DialogKind::DesignObject(_) + )); + plain(&mut a, K::Esc); + assert!(a.basic_events.is_empty()); +} + +#[test] +fn partial_save_and_external_edit_recover_without_losing_documents() { + let t = Temp::new(); + let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap(); + replace_code(&mut a, "PRINT 1\n"); + menu(&mut a, Command::NewModule); + field(&mut a, 0, "Second"); + submit(&mut a); + replace_code(&mut a, "PRINT 2\n"); + let ids = a.project.members(); + menu(&mut a, Command::SaveProject); + field(&mut a, 0, &t.0.join("p.mak").display().to_string()); + field(&mut a, 2, &t.0.join("first.bas").display().to_string()); + field( + &mut a, + 4, + &t.0.join("absent/second.bas").display().to_string(), + ); + plain(&mut a, K::Enter); + assert!(!a.dialog.as_ref().unwrap().error.is_empty()); + assert!(!a.project.document(ids[0]).unwrap().is_dirty()); + assert!(a.project.document(ids[1]).unwrap().is_dirty()); + assert!(a + .project + .document(ids[1]) + .unwrap() + .code() + .contains("PRINT 2")); + assert!(!t.0.join("p.mak").exists()); + field(&mut a, 4, &t.0.join("second.bas").display().to_string()); + submit(&mut a); + open_code(&mut a, "first.bas"); + replace_code(&mut a, "PRINT 3\n"); + fs::write(t.0.join("first.bas"), "' external\n").unwrap(); + menu(&mut a, Command::SaveFile); + plain(&mut a, K::Enter); + assert!(a + .dialog + .as_ref() + .unwrap() + .error + .contains("Überschreibentscheidung")); + assert_eq!( + fs::read_to_string(t.0.join("first.bas")).unwrap(), + "' external\n" + ); + assert!(code(&a).contains("PRINT 3")); + plain(&mut a, K::Tab); + plain(&mut a, K::Char(' ')); + submit(&mut a); + assert!(!a.project.is_dirty()); + menu(&mut a, Command::Start); + tick(&mut a, 0); + assert_eq!(a.execution, Execution::Ended); + assert_eq!(snapshot::text(a.session.screen()), " 3 \n 2 \n"); +} + +#[test] +fn reference_matrix_is_complete_and_points_to_runnable_tests() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let changes = root.join("openspec/changes"); + let name = "phase-5-08-integration-und-phasenabnahme"; + let active = changes.join(name); + let change = if active.exists() { + active + } else { + fs::read_dir(changes.join("archive")) + .unwrap() + .map(|p| p.unwrap().path()) + .find(|p| p.file_name().unwrap().to_string_lossy().ends_with(name)) + .unwrap() + }; + let overview = fs::read_to_string( + changes + .join("archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md"), + ) + .unwrap(); + let overview = overview + .split("## Vollständige Zuordnung der Referenzbedienung") + .nth(1) + .unwrap() + .split("## Explizite") + .next() + .unwrap(); + let matrix = fs::read_to_string(change.join("befehlsmatrix.md")).unwrap(); + let rows = |s: &str| { + s.lines() + .filter(|l| l.starts_with('|')) + .skip(2) + .map(|l| { + l.split('|') + .skip(1) + .map(|c| c.trim().to_owned()) + .collect::>() + }) + .collect::>() + }; + let expected = rows(overview); + let actual = rows(&matrix); + assert_eq!(expected.len(), 45); + assert_eq!(actual.len(), expected.len()); + for (expected, actual) in expected.iter().zip(actual) { + assert_eq!(&actual[..3], &expected[..3]); + let tests: Vec<_> = actual[3].split('`').skip(1).step_by(2).collect(); + assert!(!tests.is_empty()); + for test in tests { + let (file, function) = test.split_once("::").unwrap(); + let source = fs::read_to_string(root.join(file)).unwrap(); + assert!( + source.contains(&format!("#[test]\nfn {function}(")), + "{test}" + ); + } + } +} diff --git a/crates/tb-cli/src/main.rs b/crates/tb-cli/src/main.rs index 6befeb6..1ea5f58 100644 --- a/crates/tb-cli/src/main.rs +++ b/crates/tb-cli/src/main.rs @@ -336,3 +336,6 @@ impl Host for PipeHost { self.start.elapsed().as_millis() as u64 } } + +#[cfg(test)] +mod ide_acceptance; diff --git a/crates/tb-ide/src/designer.rs b/crates/tb-ide/src/designer.rs index eee3a36..df428be 100644 --- a/crates/tb-ide/src/designer.rs +++ b/crates/tb-ide/src/designer.rs @@ -720,14 +720,14 @@ impl App { for f in &input.forms { catalog.append(&f.catalog()); } - ensure!( - !catalog - .objects - .iter() - .any(|o| o.name.eq_ignore_ascii_case(new)), - "Name {new} bereits vorhanden" - ); let form_name = self.design_form()?.root.name.clone(); + ensure!( + catalog.objects.iter().filter(|o| o.name.eq_ignore_ascii_case(new)).all(|o| { + node_key(&selected).1.is_some() && o.array && o.class == selected.class + && o.parent_form.as_deref().is_some_and(|name| name.eq_ignore_ascii_case(&form_name)) + }), + "Name {new} bereits vorhanden; nur indizierte Controls derselben Klasse und Form können ein Array bilden" + ); let target = if selected.class == Class::Form { catalog.find(&old) } else { @@ -887,27 +887,52 @@ impl App { .join("; ") ) })?; + let mut candidate_catalog = forms::FormCatalog::default(); + for form in &candidate.forms { + candidate_catalog.append(&form.catalog()); + } let bindings = |references: &[(tb_frontend::SourcePos, String, u16)], files: &[tb_frontend::source::SourceFile], + objects: &forms::FormCatalog, rename_old: bool| { let mut result = Vec::new(); for (p, n, o) in references { let mut n = n.clone(); + let object = &objects.objects[*o as usize]; + let mut name = object.name.clone(); + let mut owner = object.parent_form.clone(); if rename_old && *o == target { + name = new.to_uppercase(); if n == old { n = new.to_uppercase(); } else if let Some(r) = handlers.get(&n) { n = r.to_uppercase(); } } - result.push((files[p.source as usize].path.clone(), n, *o)); + if rename_old && selected.class == Class::Form && owner.as_deref() == Some(&old) { + owner = Some(new.to_uppercase()); + } + // Merging arrays changes catalog indices. Compare semantic object identities. + result.push(( + files[p.source as usize].path.clone(), + n, + name, + owner, + object.class.name(), + object.array, + )); } result.sort(); result }; ensure!( - bindings(&bound.objects, &source_files, true) - == bindings(&candidate_bound.objects, &candidate_compiled.sources, false), + bindings(&bound.objects, &source_files, &catalog, true) + == bindings( + &candidate_bound.objects, + &candidate_compiled.sources, + &candidate_catalog, + false + ), "Unklare oder verdeckte Objektverweise; nichts geändert" ); self.project diff --git a/crates/tb-ide/tests/designer.rs b/crates/tb-ide/tests/designer.rs index db6b3b4..f54250d 100644 --- a/crates/tb-ide/tests/designer.rs +++ b/crates/tb-ide/tests/designer.rs @@ -924,3 +924,63 @@ fn events_with_the_same_name_are_local_to_their_form() { .code() .contains("SUB Form_LOAD")); } + +#[test] +fn indexed_controls_merge_atomically_with_bindings_and_undo() { + let t = Temp::new(); + let mut a = t.app(); + let root = root(&mut a); + let first = a + .design_place(C::CommandButton, 0, root, Rect::new(1, 1, 8, 2)) + .unwrap(); + property(&mut a, "INDEX", "0"); + let target = node(&mut a, first).name; + let second = a + .design_place(C::CommandButton, 0, root, Rect::new(1, 4, 8, 2)) + .unwrap(); + let old = node(&mut a, second).name; + // Scalar and duplicate-index merges must leave every document untouched. + let before = a.design_form().unwrap().clone(); + assert!(a.design_rename(&target).is_err()); + assert_eq!(*a.design_form().unwrap(), before); + property(&mut a, "INDEX", "0"); + let before = a.design_form().unwrap().clone(); + assert!(a.design_rename(&target).is_err()); + assert_eq!(*a.design_form().unwrap(), before); + property(&mut a, "INDEX", "1"); + let unrelated = a + .design_place(C::TextBox, 0, root, Rect::new(20, 1, 8, 2)) + .unwrap(); + let unrelated_name = node(&mut a, unrelated).name; + property(&mut a, "INDEX", "2"); + let before = a.design_form().unwrap().clone(); + assert!(a.design_rename(&target).is_err()); + assert_eq!(*a.design_form().unwrap(), before); + a.design_select(second, false).unwrap(); + let doc = a.design_document().unwrap(); + let source = format!("SUB {old}_Click(Index AS INTEGER)\n{old}(1).Caption = {target}(0).Caption\n{unrelated_name}(2).Text = \"kept\"\nEND SUB\n"); + a.project.replace_text(doc, 0..0, &source).unwrap(); + let before = a.design_form().unwrap().clone(); + a.design_rename(&target).unwrap(); + assert_eq!(node(&mut a, second).name, target); + let code = &a.design_form().unwrap().code; + assert!(code + .to_uppercase() + .contains(&format!("SUB {}_CLICK", target.to_uppercase()))); + assert!(code.contains(&format!("{target}(1).Caption"))); + assert!(code.contains(&format!("{unrelated_name}(2).Text"))); + a.execute(Command::Undo); + assert_eq!(*a.design_form().unwrap(), before); + // Two handlers for the merged event are ambiguous and cannot be committed. + a.project + .replace_text( + doc, + 0..0, + &format!("SUB {target}_Click(Index AS INTEGER)\nEND SUB\n"), + ) + .unwrap(); + let before = a.design_form().unwrap().clone(); + a.design_select(second, false).unwrap(); + assert!(a.design_rename(&target).is_err()); + assert_eq!(*a.design_form().unwrap(), before); +} diff --git a/docs/ide-bedienung.md b/docs/ide-bedienung.md index 3f4346d..7b87f77 100644 --- a/docs/ide-bedienung.md +++ b/docs/ide-bedienung.md @@ -336,7 +336,10 @@ Position und Größe auch bei langen Namen/Werten. Boolean-Werte sind `true`/`false` beziehungsweise `-1`/`0`. Typen, Metadatenbereiche und Laufzeitregeln werden geprüft; schreibgeschützte Properties bleiben erhalten. Spin.Style wählt 0 (vertikal, 1×2) oder 1 (horizontal, 2×1); Width/Height -sind bei Spin schreibgeschützt. Explizites Index=0 erzeugt ein Control-Array. +sind bei Spin schreibgeschützt. Explizites Index=0 erzeugt ein Control-Array. Weitere Controls derselben +Klasse und Form erhalten zuerst einen anderen eindeutigen Index und dann +denselben CtlName. Indexkollisionen oder doppelte Ereignisprozeduren werden +ohne Teiländerung abgewiesen; Undo stellt Namen und Codebindungen wieder her. Cut/Copy/Paste/Clear und Undo wirken im Designer auf Controls; die Textzwischenablage des Editors bleibt getrennt. Container nehmen ihre diff --git a/docs/tutorial.md b/docs/tutorial.md index dec1e28..4c28af2 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -12,15 +12,22 @@ und mögliche Überschreibbestätigungen stehen im Speicherdialog. Tools → Command Button wählt das Werkzeug. Ziehe auf der Form eine Fläche auf; alternativ platziert ein Doppelklick auf das Toolbox-Werkzeug ein Control. -Wähle das Control und mit Page Up/Down die Eigenschaft Caption. F2 aktiviert +Wähle das Control. Falls die Menüleiste statt der Properties Bar sichtbar ist, +wechsle mit F10 zur Properties Bar. Page Up/Down wählt die Eigenschaft Caption. F2 aktiviert das Wertfeld, tippe `Hallo` und bestätige mit Enter. Pfeile verschieben, Shift+Pfeile verändern die Größe. Die tatsächlichen Klassennamen und Eigenschaften stehen in der [Forms-Referenz](forms-referenz.md). +Für ein Control-Array setze beim ersten Control Index auf `0`. Platziere +ein zweites Control derselben Klasse, setze dessen Index auf `1` und +CtlName auf den Namen des ersten Controls. Beide gehören nun zum selben +Array. Jeder Index muss eindeutig sein; Undo nimmt die Umbenennung +einschließlich gebundener Codeverweise zurück. + ## Ereigniscode schreiben -F12 öffnet die Ereignisauswahl des gewählten Controls. Wähle Click und -bestätige. Die IDE öffnet beziehungsweise erzeugt die passende Prozedur. +F12 öffnet die Objektauswahl. Bestätige das gewählte Control mit Enter, +wähle anschließend Click und bestätige erneut. Die IDE öffnet beziehungsweise erzeugt die passende Prozedur. Schreibe zwischen SUB und END SUB: ```basic diff --git a/openspec/changes/archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md b/openspec/changes/archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md index 7d17efc..0c68239 100644 --- a/openspec/changes/archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md +++ b/openspec/changes/archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md @@ -35,7 +35,7 @@ Besondere Risiken sind unverändert ausgeführte alte Kompilate nach einer Text | 05 | [Formular-Designer](../2026-09-06-phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode | | 06 | [Debugger und Direktfenster](../2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md) | 03, 04 | Quellgenaues Debuggen, Watches, Immediate, History und Fehlerhalte | | 07 | [Hilfesystem](../2026-09-07-phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation | -| 08 | [Integration und Phasenabnahme](../../phase-5-08-integration-und-phasenabnahme/proposal.md) | 01–07 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status | +| 08 | [Integration und Phasenabnahme](../2026-09-07-phase-5-08-integration-und-phasenabnahme/proposal.md) | 01–07 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status | ```text 01 --> 02 --> 03 --> 04 --> 06 --+ diff --git a/openspec/changes/phase-5-08-integration-und-phasenabnahme/.openspec.yaml b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/.openspec.yaml similarity index 100% rename from openspec/changes/phase-5-08-integration-und-phasenabnahme/.openspec.yaml rename to openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/.openspec.yaml diff --git a/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/befehlsmatrix.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/befehlsmatrix.md new file mode 100644 index 0000000..6eefbbd --- /dev/null +++ b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/befehlsmatrix.md @@ -0,0 +1,65 @@ +# Phase 5: ausführbare Befehlsmatrix + +Die ersten drei Spalten übernehmen alle 45 Zeilen der ursprünglichen Phase-5-Übersicht. +Zuständigkeiten 01–07 bezeichnen die archivierten Featurechanges, 08 die Integration. +Alle hier genannten Rust-Tests laufen mit `cargo test --workspace`. +`reference_matrix_is_complete_and_points_to_runnable_tests` prüft Vollständigkeit, +Zuordnung und vorhandene Testfunktionen auch nach Archivierung dieses Changes. + +Der gemeinsame App-Nachweis `every_menu_command_has_native_keyboard_and_mouse_routing` +speist für jeden Menüeintrag in Environment und Designer sowohl Alt/Mnemonic/Pfeile/Enter +als auch gerenderte Maus-Hitbereiche ein. Verfügbarkeitsdiagnosen sind dort erlaubt; +den tatsächlichen Facheffekt im zulässigen Kontext belegen die folgenden Fachtests. +`function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` und der Hauptablauf +prüfen zusätzlich die kontextabhängigen Funktionstasten ohne doppelte BASIC-Zustellung. +Kein interner Befehlsaufruf allein wird als Nachweis des nativen Eingaberoutings gewertet. +File-Shell und BASIC-Shell werden zusätzlich durch `python3 tests/support/ide-execution-pty.py` +im echten lokalen PTY geprüft. Die Emulator-/OS-Matrix bleibt Phase 6. + +| Referenzbereich | Befehle / Bedienung | Zuständig | Ausführbare Verhaltensnachweise | +|---|---|---|---| +| File | New Project, Open Project, Save Project | 01, 02 | `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` | +| File | New Form, New Module, Add File, Remove File | 01, 02 | `crates/tb-ide/tests/app.rs::file_picker_save_conflict_and_project_replacement_use_real_events`; `crates/tb-ide/tests/documents.rs::startup_removal_requires_explicit_replacement_and_never_deletes_a_file` | +| File | Save File, Save File As, Load Text, Save Text | 01; Textbearbeitung 03 | `crates/tb-ide/tests/app.rs::file_picker_save_conflict_and_project_replacement_use_real_events`; `crates/tb-ide/tests/documents.rs::shared_views_text_import_and_undo_are_document_transactions`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` | +| File | Print | 04 | `crates/tb-ide/tests/execution.rs::print_uses_utf8_protected_output_and_does_not_mutate_source` | +| File | Shell | 04 | `crates/tb-ide/tests/execution.rs::shell_request_is_resumed_once_and_error_is_basic_error` | +| File | Exit / Alt+F4 | 02; Dokumententscheidungen 01, Sitzungsende 04 | `crates/tb-ide/tests/app.rs::exit_with_partial_save_retains_dialog_and_unsaved_documents` | +| Edit | Undo/Ctrl+Z, Cut/Ctrl+X, Copy/Ctrl+C, Paste/Ctrl+V, Clear/Del | Text 03, Controls 05 | `crates/tb-ide/tests/editor.rs::cua_unicode_overwrite_wordstar_and_shared_undo`; `crates/tb-ide/tests/designer.rs::clipboard_children_arrays_and_delete_preserve_code` | +| Edit | New Sub, New Function | 03 | `crates/tb-ide/tests/editor.rs::procedures_and_shared_save_hook_preserve_drafts_and_frm_code` | +| Edit | Event Procedures / F12 | 05, Codeoperationen 03 | `crates/tb-ide/tests/designer.rs::event_array_signature_twice_and_shared_form_identity`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` | +| View | Code / F2, Form / Shift+F12 | 02, 03, 05 | `crates/tb-cli/src/ide_acceptance.rs::function_keys_conflict_contexts_and_errors_keep_a_single_input_owner`; `crates/tb-ide/tests/designer.rs::event_array_signature_twice_and_shared_form_identity` | +| View | Next Statement | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart` | +| View | Output Screen / F4 | 04 | `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors` | +| View | Included File, Included Lines | 03 | `crates/tb-ide/tests/editor.rs::includes_and_diagnostic_selection_use_original_physical_files` | +| View (Designer) | Menu Bar / F10, Grid Lines | 05 auf 02 | `crates/tb-ide/tests/designer.rs::property_bar_types_readonly_and_no_status_line`; `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection` | +| Search | Find, Selected Text/Ctrl+Backslash, Repeat Last Find/F3, Change | 03 | `crates/tb-ide/tests/editor.rs::search_scope_direction_wrap_and_shortening_replace_all` | +| Run | Start/Shift+F5, Restart, Continue/F5, Modify COMMAND$ | 04 | `crates/tb-ide/tests/execution.rs::restart_command_run_targets_and_missing_target_preserve_documents`; `crates/tb-cli/src/ide_acceptance.rs::saved_project_ide_source_cli_and_tbc_have_identical_sessions` | +| Run | Unterbrechen / Ctrl+Break | 04; zusätzlicher zugänglicher IDE-Befehl | `crates/tb-ide/tests/execution.rs::f5_input_pause_copy_and_single_delivery_keep_the_same_vm` | +| Run | Set Start-up File | Persistenz 01, Ausführungswirkung 04 | `crates/tb-ide/tests/execution.rs::startup_selection_matches_shared_cli_pipeline_and_legacy_mak` | +| Run | Make EXE File, Make Library | Vollständige UI 02, UI-Abnahme 08; native Erzeugung und reale Backend-Anbindung Phase 6 | `crates/tb-ide/tests/app.rs::export_dialogs_validate_and_render_all_results_without_creating_files`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` | +| Debug | Add Watch, Instant Watch/Shift+F9, Watchpoint, Delete Watch, Delete All Watch | 06 | `crates/tb-ide/tests/debugger.rs::dispatcher_marks_edits_rebind_and_readonly_watches`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned`; `crates/tb-ide/tests/debugger.rs::calls_mouse_inspection_instant_key_delete_and_error_window` | +| Debug | Trace On, History On, Shift+F8/Shift+F10 | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` | +| Debug | Toggle Breakpoint/F9, Clear All Breakpoints, Break on Errors, Set Next Statement | 06 | `crates/tb-ide/tests/debugger.rs::dispatcher_marks_edits_rebind_and_readonly_watches`; `crates/tb-vm/tests/debugger.rs::immediate_handler_resume_and_break_on_errors`; `crates/tb-vm/tests/debugger.rs::set_next_rejects_loop_gosub_handler_and_pending_operations_atomically` | +| Debug-Tasten | F7 bis Cursor, F8 Single Step, F10 Procedure Step | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart`; `crates/tb-vm/tests/debugger.rs::gosub_step_over_breakpoint_priority_and_temporary_target_cleanup` | +| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking | 02; Wirkung in 01/03/07 | `crates/tb-ide/tests/app.rs::options_apply_persist_across_bases_and_preserve_unsaved_values_on_error`; `crates/tb-ide/tests/editor.rs::line_leave_incomplete_invalid_disabled_and_current_revisions`; `crates/tb-ide/tests/help.rs::right_click_configuration_and_external_link_follow` | +| Window | New Window, Arrange All, offene Codefensterliste | 02 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss` | +| Window | Calls, Debug, Immediate | Fenster 02, Inhalt 06 | `crates/tb-ide/tests/debugger.rs::calls_mouse_inspection_instant_key_delete_and_error_window`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` | +| Window | Help, Output, Project | Fenster 02, Inhalt 07/04/01 | `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path`; `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors`; `crates/tb-ide/tests/help.rs::links_history_resize_scroll_and_focus_use_real_events` | +| Window (Designer) | Color Palette, Menu Design Window, Toolbox, Help, Formliste | 05, Hilfe 07 | `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection`; `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected`; `crates/tb-ide/tests/help.rs::help_preserves_fullscreen_and_does_not_modify_designer_or_source` | +| Help | Index, Contents, Keyboard, Topic/F1, Using Help/Shift+F1, Tutorial, About | 07 | `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` | +| Menüs | Alt/F11, Mnemonic, Esc, Dialogkennzeichnung „…“, Help rechts | 02 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-cli/src/ide_acceptance.rs::every_menu_command_has_native_keyboard_and_mouse_routing` | +| Fenster | F6/Ctrl+F6, Shift+F6, Ctrl+F4, Ctrl+F7/F8/F9/F10/F5, Alt+Minus | 02 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss` | +| Projektfenster | Anfangs rechts, Form/Code-Buttons, Enter | 02 auf 01 | `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path`; `crates/tb-ide/tests/app.rs::clicking_project_members_and_dialog_choices_transfers_focus_and_applies_values` | +| Toolbox/Tools | Check Box, Combo Box, Command Button, Dir List, Drive List, File List, Frame, HScrollBar, Label, List Box, Option Button, Picture Box, Text Box, Timer, VScrollBar; zusätzlich bereits unterstützte Spinvarianten | 05 | `crates/tb-ide/tests/designer.rs::all_metadata_tools_persist_and_preview_matches_runtime_without_events` | +| Designgesten | Toolbox-Doppelklick, Aufziehen, Drag, Sizing Handles, Pfeile, Ctrl+Pfeile, Shift+Pfeile, Tab/Shift+Tab, Ctrl+Klick | 05 | `crates/tb-ide/tests/designer.rs::container_multiselect_keyboard_drag_resize_and_undo`; `crates/tb-ide/tests/designer.rs::all_eight_handles_resize_transactionally_and_preserve_identity_after_structure_edits`; `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection` | +| Properties Bar | Property/Value-Dropdown, Koordinaten/Größe, F2 Value, F10 Barwechsel, keine Statuszeile | 05 auf 02 | `crates/tb-ide/tests/designer.rs::property_bar_types_readonly_and_no_status_line`; `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected` | +| Menüdesigner | Caption (&/-), CtlName, Tag, Index, Checked/Enabled/Visible/Separator, Shortcut, Einfügen/Löschen/Ebene/Reihenfolge/Done | 05 | `crates/tb-ide/tests/designer.rs::menu_hierarchy_arrays_shortcuts_and_palette_use_events`; `crates/tb-ide/tests/designer.rs::menu_indent_outdent_rename_and_field_changes_have_one_undo` | +| Farbpalette | ForeColor/BackColor, Drag auf Form | 05 | `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected` | +| Editor | Line-Leave-Prüfung/-Normalisierung, Syntax-Checking-Toggle, DECLARE, DEFtype | 03 | `crates/tb-ide/tests/editor.rs::line_leave_incomplete_invalid_disabled_and_current_revisions`; `crates/tb-ide/tests/editor.rs::external_include_revision_and_project_context_declarations` | +| Editor-Tasten | CUA, Einfg, Pos1/Ende, WordStar Ctrl+Q, Ctrl+Y, Bookmarks Ctrl+K,0–3 / Ctrl+Q,0–3, Shift+F2/Ctrl+F2 | 03 | `crates/tb-ide/tests/editor.rs::cua_unicode_overwrite_wordstar_and_shared_undo`; `crates/tb-ide/tests/editor.rs::crlf_navigation_word_boundaries_readonly_and_normalization_keep_views` | +| Immediate | PRINT, Zuweisungen, Prozeduraufrufe, ERROR n im Break-Modus | 06 | `crates/tb-vm/tests/debugger.rs::watches_immediate_byref_and_errors_preserve_continuation`; `crates/tb-vm/tests/debugger.rs::immediate_wait_interrupt_nested_error_end_and_run`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` | +| Hilfe | F1/Rechtsklick, Shift+F1, ◄Thema►, Tab/Shift+Tab, Enter, Alt+F1 (20), Ctrl+F1, Esc | 07 | `crates/tb-ide/tests/help.rs::links_history_resize_scroll_and_focus_use_real_events`; `crates/tb-ide/tests/help.rs::right_click_configuration_and_external_link_follow`; `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` | +| Darstellung | DOS-Farben, Magenta-Titel, Desktopzeichen, Tabweite, klickbare Statuskürzel, Zeile/Spalte | 02 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path` | +| Start | Begrüßung, Version/Copyright, Untitled-Code/Projekt, keine Easy/Full-Modi | 02, About 07 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` | +| Terminal | Dynamische Größe, physisches Minimum 80×25 | 02; Runtime-/Output-Abgrenzung 04 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss`; `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors`; `crates/tb-cli/src/ide_acceptance.rs::function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` | +| Laufzeitparität | Dieselbe TBVM in IDE, CLI und TBC | 04/06; Gesamtnachweis 08 | `crates/tb-cli/src/ide_acceptance.rs::saved_project_ide_source_cli_and_tbc_have_identical_sessions` | diff --git a/openspec/changes/phase-5-08-integration-und-phasenabnahme/design.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/design.md similarity index 100% rename from openspec/changes/phase-5-08-integration-und-phasenabnahme/design.md rename to openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/design.md diff --git a/openspec/changes/phase-5-08-integration-und-phasenabnahme/proposal.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/proposal.md similarity index 62% rename from openspec/changes/phase-5-08-integration-und-phasenabnahme/proposal.md rename to openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/proposal.md index 7a2eee8..aaf5c25 100644 --- a/openspec/changes/phase-5-08-integration-und-phasenabnahme/proposal.md +++ b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/proposal.md @@ -24,6 +24,6 @@ Keine bestehenden Anforderungen werden ersetzt; die neue IDE-Fähigkeit ergänzt tb-ide-Integrationstests, vorhandene CLI-/VM-/Forms-Tests und Benchmarks, docs/ sowie PLAN.md. Featureimplementierung bleibt jeweils beim verantwortlichen Change; dieser Change schließt Integration und Nachweise. -**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../archive/2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../archive/2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../archive/2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../archive/2026-09-06-phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../archive/2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../archive/2026-09-07-phase-5-07-hilfesystem/proposal.md). +**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../2026-09-06-phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../2026-09-07-phase-5-07-hilfesystem/proposal.md). -**Gesamtplanung:** [Phase-5-Übersicht](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch. +**Gesamtplanung:** [Phase-5-Übersicht](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch. diff --git a/openspec/changes/phase-5-08-integration-und-phasenabnahme/specs/ide-phasenabnahme/spec.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/specs/ide-phasenabnahme/spec.md similarity index 100% rename from openspec/changes/phase-5-08-integration-und-phasenabnahme/specs/ide-phasenabnahme/spec.md rename to openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/specs/ide-phasenabnahme/spec.md diff --git a/openspec/changes/phase-5-08-integration-und-phasenabnahme/tasks.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/tasks.md similarity index 72% rename from openspec/changes/phase-5-08-integration-und-phasenabnahme/tasks.md rename to openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/tasks.md index 68859b2..a706e62 100644 --- a/openspec/changes/phase-5-08-integration-und-phasenabnahme/tasks.md +++ b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/tasks.md @@ -1,21 +1,21 @@ ## 1. Vollständiger Arbeitsablauf -- [ ] 1.1 Den headless App-Testpfad aus 02 für Datei-/Dialogaktionen und deterministische VM-Ereignisse verwenden; ein Einstiegstest startet die echte App und erstellt ein leeres Projekt ohne vorbereitete Dokumentobjekte. -- [ ] 1.2 Den Hauptablauf mit zwei BAS-Modulen, FRM, Control-Array, Menü, Include, F12-Ereigniscode und Startdatei über UI-Ereignisse aufbauen; Save/Close/Reopen muss den gleichen Projekt-/Formularstand ergeben. -- [ ] 1.3 Den Ablauf um Start, Include-Breakpoint, Prozedurschritt, Watch, Immediate-Wertänderung und Continue ergänzen; Sollausgabe und Variablenwerte belegen die erwartete Wirkung. -- [ ] 1.4 F1 aus Editor und Designer sowie Tab/Enter/Alt+F1 in denselben Ablauf aufnehmen; Hilfeziel und wiederhergestellter Fokus müssen stimmen. +- [x] 1.1 Den headless App-Testpfad aus 02 für Datei-/Dialogaktionen und deterministische VM-Ereignisse verwenden; ein Einstiegstest startet die echte App und erstellt ein leeres Projekt ohne vorbereitete Dokumentobjekte. +- [x] 1.2 Den Hauptablauf mit zwei BAS-Modulen, FRM, Control-Array, Menü, Include, F12-Ereigniscode und Startdatei über UI-Ereignisse aufbauen; Save/Close/Reopen muss den gleichen Projekt-/Formularstand ergeben. +- [x] 1.3 Den Ablauf um Start, Include-Breakpoint, Prozedurschritt, Watch, Immediate-Wertänderung und Continue ergänzen; Sollausgabe und Variablenwerte belegen die erwartete Wirkung. +- [x] 1.4 F1 aus Editor und Designer sowie Tab/Enter/Alt+F1 in denselben Ablauf aufnehmen; Hilfeziel und wiederhergestellter Fokus müssen stimmen. ## 2. Grenzfälle und vollständige Bedienung -- [ ] 2.1 Die gesamte Befehlsmatrix der Phase-5-Übersicht mit konkreten Tests verknüpfen und auf fehlende Einträge prüfen; alle Phase-5-Aktionen müssen im zulässigen Kontext mehr als einen Platzhaltereffekt besitzen. -- [ ] 2.2 Mehrfachbelegungen von F2/F4/F10/F11/F12 sowie Tastatur-/Menü-/Mausäquivalenz über native Eingabeevents prüfen; kein Event darf zugleich als IDE-Aktion und BASIC-Eingabe wirksam werden. -- [ ] 2.3 Kurze Fehlerabläufe für Teilspeichern, externe Änderung, Compilefehler, Pause beim Eingabewarten, RUN-Zielfehler, Shell-Rückkehr und Shrink/Resize hinzufügen; Dokumenterhalt und anschließende Bedienbarkeit müssen belegt sein. +- [x] 2.1 Die gesamte Befehlsmatrix der Phase-5-Übersicht mit konkreten Tests verknüpfen und auf fehlende Einträge prüfen; alle Phase-5-Aktionen müssen im zulässigen Kontext mehr als einen Platzhaltereffekt besitzen. +- [x] 2.2 Mehrfachbelegungen von F2/F4/F10/F11/F12 sowie Tastatur-/Menü-/Mausäquivalenz über native Eingabeevents prüfen; kein Event darf zugleich als IDE-Aktion und BASIC-Eingabe wirksam werden. +- [x] 2.3 Kurze Fehlerabläufe für Teilspeichern, externe Änderung, Compilefehler, Pause beim Eingabewarten, RUN-Zielfehler, Shell-Rückkehr und Shrink/Resize hinzufügen; Dokumenterhalt und anschließende Bedienbarkeit müssen belegt sein. -- [ ] 2.4 Beide nativen Exportdialoge vollständig über App-Ereignisse prüfen; gültige/ungültige Ziele, Überschreibentscheidung und kontrollierte Status-/Fehlerrückmeldungen funktionieren, während der Phase-5-Produktionspfad die fehlende Erzeugung erklärt und keine native Datei vortäuscht. +- [x] 2.4 Beide nativen Exportdialoge vollständig über App-Ereignisse prüfen; gültige/ungültige Ziele, Überschreibentscheidung und kontrollierte Status-/Fehlerrückmeldungen funktionieren, während der Phase-5-Produktionspfad die fehlende Erzeugung erklärt und keine native Datei vortäuscht. ## 3. Parität, Leistung und Abschluss -- [ ] 3.1 Dasselbe gespeicherte Projekt als IDE-Lauf, CLI-Quelllauf und TBC-Lauf mit identischem Hostskript vergleichen; Ausgabe, Anfangszustand, Dateieffekte und Fehler müssen bis auf dokumentierte IDE-Halte übereinstimmen. -- [ ] 3.2 Die vorhandenen relevanten Workspace-Regressionen und Release-Compile-Benchmarks einmal auf dem finalen Stand ausführen; Ergebnisse, Referenzhardware, Profil, Größen und Zeitgrenzen in verification.md festhalten. -- [ ] 3.3 Die endgültige IDE-Bedienung mit Keyboard/Tutorial/Hilfe abgleichen und PLAN.md ausschließlich anhand bestandener Nachweise aktualisieren; die Zuordnung jeder Phase-5-Checkbox und die separat offene Phase-6-Plattformmatrix sind im Abschlussbericht prüfbar. -- [ ] 3.4 Alle acht Changes zusammen gegen die Hauptspezifikation und Befehlsmatrix verifizieren; offene Befunde an ihrem zuständigen Change beheben und den betroffenen Nachweis erneut ausführen, bis keine Phase-5-Befunde übrig sind. +- [x] 3.1 Dasselbe gespeicherte Projekt als IDE-Lauf, CLI-Quelllauf und TBC-Lauf mit identischem Hostskript vergleichen; Ausgabe, Anfangszustand, Dateieffekte und Fehler müssen bis auf dokumentierte IDE-Halte übereinstimmen. +- [x] 3.2 Die vorhandenen relevanten Workspace-Regressionen und Release-Compile-Benchmarks einmal auf dem finalen Stand ausführen; Ergebnisse, Referenzhardware, Profil, Größen und Zeitgrenzen in verification.md festhalten. +- [x] 3.3 Die endgültige IDE-Bedienung mit Keyboard/Tutorial/Hilfe abgleichen und PLAN.md ausschließlich anhand bestandener Nachweise aktualisieren; die Zuordnung jeder Phase-5-Checkbox und die separat offene Phase-6-Plattformmatrix sind im Abschlussbericht prüfbar. +- [x] 3.4 Alle acht Changes zusammen gegen die Hauptspezifikation und Befehlsmatrix verifizieren; offene Befunde an ihrem zuständigen Change beheben und den betroffenen Nachweis erneut ausführen, bis keine Phase-5-Befunde übrig sind. diff --git a/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/verification.md b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/verification.md new file mode 100644 index 0000000..fedb4ed --- /dev/null +++ b/openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/verification.md @@ -0,0 +1,165 @@ +# Verification Report: phase-5-08-integration-und-phasenabnahme + +Stand: 2026-09-07. Geprüft wurden Proposal, Design, sämtliche Aufgaben, das +Delta `ide-phasenabnahme` sowie die sieben synchronisierten Phase-5-Hauptspecs. +Der zusätzliche Abgleich umfasst sämtliche 45 Zeilen der Referenzbedienung. + +## Ergebnis + +| Dimension | Ergebnis | +| --- | --- | +| Completeness | 12/12 Aufgaben; 5/5 Anforderungen des Changes; 45/45 Matrixzeilen | +| Correctness | 5/5 neue Szenarien; zusammen mit 01–07 insgesamt 47 Anforderungen und 67 Szenarien | +| Coherence | Bestehende App, Dokumenttransaktionen, Compiler, VM, CLI und TestBackend wiederverwendet | +| Offene Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION | + +## Anforderungen und neue Nachweise + +Alle sechs neuen Tests liegen in `crates/tb-cli/src/ide_acceptance.rs`. +Sie laufen über `cargo test -p tb-cli --bin tbc ide_acceptance` und im +Workspace-Lauf. Als Testmodul des bestehenden CLI-Einstiegs können sie dessen +wirkliche `cmd_build`, `compile` und `run_chain` verwenden. Die bereits im +Workspace vorhandenen crossterm-/ratatui-Abhängigkeiten sind dafür zusätzlich +als CLI-Testabhängigkeiten deklariert; es gibt keine neue Laufzeitabhängigkeit. + +| Requirement / Scenario | Implementierung und bestandener Nachweis | +| --- | --- | +| Vollständiger IDE-Arbeitsablauf / Schreiben bis Wiederaufnahme | `create_save_debug_help_exports_and_reopen` startet `App::new` ohne vorbereitete Dokumente. Native Tastatur-, Paste- und Mausereignisse erzeugen main.bas, helper.bas, Form1.frm, ein zweielementiges CommandButton-Array, ein Menü, work.bi sowie project.mak. F12 erzeugt Form_Load über Objekt- und Ereignisauswahl. Nach Startup-/Save-Dialog, Projektwechsel und Öffnen stimmen geladene BAS-Texte, vollständiges FormFile und Startdatei mit den gespeicherten Dateien überein; der wieder geöffnete Stand läuft erneut. | +| Arbeitsablauf: Debugger und Hilfe | Derselbe Test öffnet das Include über View → Included File, setzt F9 an Zeile 2, hält vor dem modulübergreifenden Bump-Aufruf und führt ihn mit F10 bis Zeile 3 aus. Add Watch zeigt total=2. Window → Immediate setzt total=9; F5 und Eingabe abc führen zu `? abc\n 9 abc!\n` und Dateiwert 9. Sämtliche gespeicherten Quellbytes bleiben unverändert; Wiederöffnen läuft wieder mit 2. F1 im Designer und auf PRINT im Editor, Tab/Enter/Alt+F1/Esc prüfen Hilfeziel und restaurierten Fokus. | +| Nachgewiesene Befehlsabdeckung / Mehrfach belegte Funktionstasten | `every_menu_command_has_native_keyboard_and_mouse_routing` bedient jeden Menüeintrag beider Modi über Alt/Mnemonic/Pfeile/Enter sowie gerenderte Maus-Hitbereiche. `function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` und der Hauptablauf prüfen F2/F4/F10/F11/F12 einschließlich Designer-Value, Menü, Code-/Ereignisauswahl, Output-Vollbild und Debug-Prozedurschritt. IDE-Kommandos gelangen nicht zusätzlich in die BASIC-Eingabe. Die Facheffekte und zulässigen Kontexte stehen in der separaten, vollständig ausgefüllten [Befehlsmatrix](befehlsmatrix.md). `reference_matrix_is_complete_and_points_to_runnable_tests` vergleicht alle drei Originalspalten der 45 Referenzzeilen und prüft jeden verlinkten Rust-Test auch nach Archivierung. | +| CLI-Parität und robuste Übergänge / Vergleich derselben Sitzung | `saved_project_ide_source_cli_and_tbc_have_identical_sessions` erstellt dasselbe Projekt durch die UI, baut mit der tatsächlichen CLI-Funktion ein TBC und vergleicht IDE, Quell-CLI und TBC-CLI. Auf allen Wegen: 80×25, Uhrstart 0, Eingabe abc/Enter bei 100 ms, identische Anfangsdatei und COMMAND$ (`!` sowie `error`). Ausgabe, berechneter Wert 2, Dateibytes und Resultat END beziehungsweise Fehler 5 stimmen überein. Anfangs sind Form sichtbar, Datei 1 geschlossen und IDE-Eingaben leer; der zusätzliche IDE-Neustart liefert wieder dieselben Resultate. Der deterministische Host ersetzt ausschließlich das Terminal, nicht den CLI-Ausführungspfad. | +| Robuste Übergänge | `partial_save_and_external_edit_recover_without_losing_documents` erzeugt zwei Module ausschließlich über die UI. Ein fehlendes Zielverzeichnis lässt nur das erste Dokument gespeichert, das zweite bearbeitet und das Manifest ungeschrieben; Reparatur im offenen Dialog gelingt. Eine externe Änderung bleibt bis zur expliziten Überschreibentscheidung erhalten; anschließend läuft der erhaltene Projektcode. Der Funktionstasten-/Fehlertest ergänzt Compilefehler, Pause bei INPUT, fehlendes RUN-Ziel, Shell-Callback und Shrink auf 40×10 mit Rückkehr auf 100×30. Reale Shell-Übergabe und Terminalwiederherstellung werden zusätzlich im PTY geprüft. | +| Leistungs- und Dokumentationsabnahme / Abschlussbericht | Alle Workspace-Regressionen, Clippy, Formatprüfung, lokale PTY-Fälle und das vorhandene Release-Compile-Budget sind bestanden; Befehle, Ergebnisse, Hardware und Revision stehen unten. PLAN.md ordnet alle elf Phase-5-Punkte dieser Abnahme zu. Tutorial, Tastatur, Bedienungsreferenz und eingebettete Hilfe wurden auf die endgültigen Eingabewege abgeglichen. | +| Export-UI / Exportvorbereitung ohne Scheinerfolg | Im Hauptablauf werden Make EXE und Make Library über Menüs geöffnet. System/Architektur, leerer Pfad, abgewiesenes TBC-Ziel, bestehende Datei mit ausdrücklicher Überschreibentscheidung und gültiges neues Ziel werden über App-Eingaben geprüft. Das vorhandene `export_dialogs_validate_and_render_all_results_without_creating_files` ergänzt weitere Pfad-/Projektkollisionen. Die Produktionsaktion erklärt Phase 6 und schreibt keine Datei. Kontrollierte Running/Failed/Cancelled/Success-Rückmeldungen prüfen die vorhandene Auftrags-/Ergebnisübergabe; sie sind ausschließlich UI-Tests. Esc erhält den Projektstempel und alle ursprünglichen Dateibytes. | + +## Gesamtprüfung der acht Phase-5-Changes + +Die folgenden synchronisierten Spec-Dateien wurden mit ihren Implementierungs- +und Testpfaden abgeglichen. Die archivierten Berichte enthalten die detaillierte +Zuordnung der Vorgängerszenarien; ihre Tests wurden auf dem jetzigen Stand erneut +im Workspace ausgeführt. Die neue Befehlsmatrix ergänzt den gemeinsamen nativen +Eingabepfad und verlinkt konkrete ausführbare Facheffekte jeder Referenzzeile. + +| Change / Capability | Anforderungen / Szenarien | Zuständiger Code und erneuter Nachweis | +| --- | --- | --- | +| 01 / ide-projekte | 5 / 9 | `tb-ide/src/documents.rs`, `tb-vm/src/project_io.rs`; 14 Dokumenttests, CLI-Projekttests, nativer Hauptablauf und Save-Fehlerablauf | +| 02 / ide-oberflaeche | 7 / 11 | `tb-ide/src/app.rs`, `commands.rs`, `render.rs`, `export.rs`; 12 App-Tests, alle Menürouten, F-Tastenkonflikte, Export-UI und PTY | +| 03 / ide-editor | 7 / 10 | `tb-ide/src/editor.rs`, Frontend-Editing und Projektcompiler; 7 Editortests, VM-Inkrementaltests, Include-Debugablauf und Release-Benchmark | +| 04 / ide-ausfuehrung | 6 / 9 | `tb-ide/src/execution.rs`, `tb-vm/src/project_io.rs`, CLI `run_chain`; 12 Ausführungstests, tatsächliche Dreiweg-Parität, Reset-/Fehlerabläufe und PTY | +| 05 / ide-formulardesigner | 6 / 6 | `tb-ide/src/designer.rs`, gemeinsame FormFile-/Forms-Metadaten; 18 Designertests einschließlich neuer Array-Merge-Gegenproben, UI-Hauptablauf und gespeicherter CLI-Lauf | +| 06 / ide-debugger | 7 / 11 | `tb-ide/src/debugger.rs`, VM-Debugger; 6 IDE- und 14 VM-Debuggertests, tatsächlicher F9/F10/Watch/Immediate/F5-Ablauf | +| 07 / ide-hilfe | 4 / 6 | `tb-ide/src/help.rs`, native F1-Aufrufer, eingebettete docs; 11 Hilfetests, Offline-Kindprozess sowie Hauptablauf mit Editor-/Designerhilfe | +| 08 / ide-phasenabnahme | 5 / 5 | Sechs neue CLI-Abnahmetests, Befehlsmatrix, PLAN-Zuordnung und dieser Bericht | + +Keine Phase-5-Platzhalteraktion ist mehr vorhanden. Die einzige weiterhin +vorbereitete, ausdrücklich noch nicht ausführbare Erzeugungsleistung betrifft +das in Phase 6 geplante native Exportbackend. + +## Behobene Befunde + +1. **Control-Array über CtlName nicht herstellbar (Featurebereich 05).** + Die gemeinsame Umbenennung lehnte jeden bereits vorhandenen Namen ab. + Jetzt dürfen explizit indizierte Controls derselben Klasse und Form zu + einer Arraygruppe zusammengeführt werden. Formvalidierung und vollständige + Kandidatenübersetzung prüfen Index- und Ereigniskollisionen vor dem Commit. + Die Bindungsprüfung verwendet die semantische Objektidentität statt des + durch Zusammenführen verschobenen numerischen Katalogindex. + `indexed_controls_merge_atomically_with_bindings_and_undo` prüft erfolgreiche + Zusammenführung samt Ziel-/Quell-/unbeteiligten Codebindungen, vollständiges + Undo sowie atomare Ablehnung von Skalar-, Index-, Klassen- und Handlerkonflikten. + Alle bisherigen Umbenennungs-/Include-/Schattenbindungsregressionen bestehen. +2. **Tutorial verkürzte die tatsächliche Bedienfolge (Featurebereich 07).** + F12 zeigt zuerst die Objekt-, danach die Ereignisauswahl. Das Tutorial nennt + jetzt beide Bestätigungen und den nötigen F10-Wechsel zur Properties Bar + nach Menübedienung. Control-Array-Erzeugung und ihre Grenzen sind in Tutorial + und Bedienungsreferenz beschrieben. Help-Katalog und native Aufrufer bestehen. + +Die fachliche Korrektur liegt im bestehenden Designerpfad; die Integration +führt keinen zweiten Designer und keinen alternativen Compiler-/VM-Pfad ein. +Es wurden keine Fachanforderungen abgeschwächt und keine bisherigen Tests entfernt. + +## PLAN-Zuordnung + +Alle elf Checkboxen unter Phase 5 sind anhand folgender bestandener Nachweise +abgehakt; die Bezeichnung „Eigenschaftenfenster“ wurde zu „Properties Bar“ präzisiert. + +| Phase-5-Punkt | Nachweis | +| --- | --- | +| IDE-Rahmen | App-Tests, vollständige native Menürouten, Resize-/Fokusprüfung | +| Editor | Editortests, Help-Aufrufer, Include-Navigation, Inkrementaltests und Compile-Budget | +| Projektverwaltung | UI-Hauptablauf, drei persistierte Mitglieder, Save/Close/Reopen, CLI-/TBC-Parität | +| Formular-Designer | Vollständige Metadaten-Toolbox, 18 Designerregressionen, Array/Menü/Form_Load im UI-Ablauf | +| Ausführen aus der IDE | Start/Pause/Continue/Restart, Fehlerabläufe, deterministische Dreiweg-Parität | +| Export-Bedienung | Beide vollständigen Dialoge im Hauptablauf und App-Exportregression | +| `$INCLUDE` | Bestehende relative/Zyklus-/Overlayprüfungen; physischer Include-Breakpoint und Dreiweg-Parität | +| `RUN` | Bestehende Ziel-/Resetprüfungen, fehlendes Ziel im nativen Fehlerablauf und sauberer Neustart | +| Debugger | IDE-/VM-Debuggertests und tatsächliche Watch-Wertänderung mit F10/F5 im Hauptablauf | +| Hilfe-System | Elf Hilfetests, Offline-Kindprozess, Kataloglinks und beide tatsächlichen F1-Aufrufer | +| Vollständiges IDE-Programm | `create_save_debug_help_exports_and_reopen` und anschließender Dreiweg-Lauf derselben gespeicherten Dateien | + +## Abschlussprüfungen und Messung + +Geprüfter Code: Basisrevision `cc828308893b95fb614fbd2ec102a811358c7762` +plus der uncommittete Implementierungsstand dieses Changes. Keine nachträgliche +Produktcodeänderung nach dem finalen Workspace-/Clippy-/Benchmark-Lauf. +Implementierungsfingerabdruck SHA-256: `b616a1b94ce60bb215b104e5c6b4fe0edde4033703dda7a2d6124fea5e9e38b2` +(Pfade lexikografisch sortiert, jeweils UTF-8-Pfad, NUL, Dateibytes, NUL: +`Cargo.lock`, `crates/tb-cli/Cargo.toml`, `crates/tb-cli/src/main.rs`, +`crates/tb-cli/src/ide_acceptance.rs`, `crates/tb-ide/src/designer.rs`, +`crates/tb-ide/tests/designer.rs`). + +Die abschließenden PLAN-/Bericht-/Task-Änderungen wurden separat validiert; +wegen des eingebetteten PLAN wurde die Hilfeprüfung danach erneut ausgeführt. + +| Befehl | Ergebnis | +| --- | --- | +| `cargo test -p tb-cli --bin tbc ide_acceptance` | 6 bestanden | +| `cargo test -p tb-ide --test designer` | 18 bestanden | +| `cargo test --workspace` | 605 bestanden plus 1 erfolgreicher Offline-Kindprozess; 0 Fehler; 2 bestehende Ignore-Fälle | +| `cargo clippy --workspace --all-targets -- -D warnings` | bestanden | +| `cargo fmt --all -- --check` | bestanden | +| `cargo build -p tb-ide` | bestanden | +| `python3 tests/support/ide-execution-pty.py` | 3 bestanden: Shell Exit 7, Ctrl+C im Shell-Kind, File → Shell; Rückkehr und Terminalrestaurierung geprüft | +| `cargo bench -p tb-vm --bench compile` | alle vier bestehenden Zeitbudgets eingehalten, einmaliger Abschlusslauf | +| `cargo test -p tb-ide --test help` | 11 bestanden plus Offline-Kindprozess, nach PLAN-Abschluss | +| `openspec validate --all --strict --no-interactive` | 23/23 gültig; bestehende INFO-Längenhinweise sind keine Fehler | +| `git diff --check` | bestanden | + +Referenzhardware: Apple M5 Max, aarch64, 18 CPU-Kerne, 128 GiB RAM; +macOS 26.6.2 (25G83). rustc 1.97.1 (8bab26f4f, 2026-07-14), +cargo 1.97.1 (c980f4866, 2026-06-30). Bestehender eigener Benchmark-Harness, +Release mit opt-level=3, LTO=true, codegen-units=1. Kein neues Benchmarkverfahren. + +| Messung | Größe / Verfahren | Ergebnis | Bestehendes Budget | +| --- | --- | --- | --- | +| Vollcompile Einzelmodul | 508 Zeilen; Aufwärmen, bester von 10 Läufen | 0,74 ms | <50 ms | +| Vollcompile Projekt | 20 Module, 49.760 Zeilen, 335.421 Instruktionen; ein Lauf | 98 ms | <1.000 ms | +| Cacheänderung Einzelmodul | Private Rumpfänderung; Median aus 7; 1 neu / 0 wiederverwendet | 0,77 ms | <50 ms | +| Cacheänderung Projekt | Private Rumpfänderung; Median aus 7; 1 neu / 19 wiederverwendet; Invalidierung + Link | 21,94 ms | <1.000 ms | + +Die beiden bestehenden Ignore-Fälle sind der explizite Golden-Output-Generator +und die externe VBDOS-Referenzsuite (`TB_VBDOS_REPO`); kein Phase-5-Test ist +ignoriert. Die normale vorhandene Workspace-Regression wurde vollständig ausgeführt. +Die lokale Abnahme verwendet native crossterm-Ereignisse, TestBackend und einen +realen Unix-PTY. Sie ersetzt nicht die separat offene Phase-6-Matrix für Windows +Terminal, mehrere Linux-Emulatoren und macOS samt systematischen Maus-/Sondertasten. +Auch native Executable-/Library-Erzeugung und deren Verbraucherprüfung bleiben +unverändert offene Phase-6-Aufgaben. Kontrollierte Exportcallbacks behaupten +keinen tatsächlichen Export. + +Keine Prüfdimension dieses Changes wurde ausgelassen. Die erneute Verifizierung +meldet keine offenen Befunde. Der Change ist zur separaten Synchronisierung und +Archivierung bereit. + +## Synchronisierung und Archivierung + +Am 2026-09-07 wurden alle fünf Anforderungen einschließlich Purpose unverändert +in `openspec/specs/ide-phasenabnahme/spec.md` synchronisiert. Der vollständige +Vergleich mit dem Delta und die strikte Validierung aller 23 Hauptspecs bestanden +vor dem Archivieren. Der Change liegt jetzt unter +`openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/`. +Abhängigkeits- und Übersichtslinks wurden an den Archivpfad angepasst. +Der ausführbare Matrixnachweis besteht auch am Archivort; OpenSpec führt +keine aktiven Changes mehr. Der oben dokumentierte Produktcode-Fingerabdruck +ist unverändert, daher wurden die bereits bestandenen Produkt- und +Leistungsprüfungen nicht erneut ausgeführt. diff --git a/openspec/specs/ide-phasenabnahme/spec.md b/openspec/specs/ide-phasenabnahme/spec.md new file mode 100644 index 0000000..c0f4f2b --- /dev/null +++ b/openspec/specs/ide-phasenabnahme/spec.md @@ -0,0 +1,42 @@ +# ide-phasenabnahme Specification + +## Purpose + +Sichert die vollständige Nutzbarkeit der Phase-5-IDE über zusammenhängende Arbeitsabläufe, gemeinsame Laufzeitsemantik und reproduzierbare Abnahmekriterien. + +## Requirements + +### Requirement: Vollständiger IDE-Arbeitsablauf +Ein Benutzer SHALL ausschließlich über die IDE ein Projekt mit mindestens zwei BAS-Modulen, einem Formular, einem Control-Array, einem Menü und einem Include erstellen, bearbeiten, speichern, schließen und erneut öffnen können. Danach SHALL dasselbe Projekt mit Ereigniscode ausführbar und mit Breakpoints, Prozedurschritt, Watch und Direktfenster debugbar sein. Kontext-Hilfe SHALL aus Editor und Designer erreichbar sein. + +#### Scenario: Schreiben bis Wiederaufnahme +- **WHEN** dieser Arbeitsablauf durch Menüs, Tastatur und Maus durchgeführt wird und während eines Halts ein Wert im Direktfenster geändert wird +- **THEN** läuft das Programm mit der Änderung weiter, die gespeicherten Quellen bleiben konsistent und nach erneutem Öffnen sind Projekt, Formularstruktur, Ereigniscode und Startdatei erhalten + +### Requirement: Nachgewiesene Befehlsabdeckung +Alle Phase-5-Funktionen und die ihnen zugeordneten Menü-, Tastatur- und Mauswege aus docs/ide-referenz.md SHALL einen zuständigen Change und einen ausführbaren Nachweis besitzen. Ein Test nur des internen Befehls SHALL keinen Nachweis für die tatsächliche Tasten-/Mausverteilung ersetzen. Noch nicht implementierte Phase-5-Aktionen MUST NOT als vollständige Phasenabnahme gelten. + +#### Scenario: Mehrfach belegte Funktionstasten +- **WHEN** die Abnahme F2, F4, F10, F11 und F12 in ihren verschiedenen Kontexten einspeist +- **THEN** bestätigt sie die jeweils erwartete Aktion und den Ausschluss einer zweiten Zustellung an das BASIC-Programm + +### Requirement: CLI-Parität und robuste Übergänge +Gespeicherte Projekte SHALL über IDE, CLI-Quellstart und CLI-Kompilatstart bei gleichen Eingaben, Zeiten, Größen und COMMAND$ dieselben BASIC-Ausgaben, Datenwerte und Fehler liefern, abgesehen von den ausdrücklich spezifizierten IDE-Halten. Speichern, Projektwechsel, Reset, Shell-Rückkehr und Resize SHALL auch nach Fehlern die Dokumente und Terminalbedienbarkeit erhalten. + +#### Scenario: Vergleich derselben Sitzung +- **WHEN** ein Forms-/Include-Projekt mit identischer Ereignisfolge über die drei Ausführungswege gestartet wird +- **THEN** stimmen Ausgabe, Dateieffekte und Laufzeitresultat überein und ein anschließender IDE-Neustart enthält keinen Zustand des vorherigen Laufs + +### Requirement: Leistungs- und Dokumentationsabnahme +Phase 5 SHALL erst als umgesetzt dokumentiert werden, wenn die Change-Szenarien, die relevanten bisherigen Regressionen und das bestehende Release-Compile-Budget nachgewiesen sind. PLAN.md und die IDE-Hilfe SHALL den verifizierten Stand wiedergeben. Prüfberichte SHALL konkrete Befehle, Ergebnisse, Revision und verbleibende Grenzen nennen; vollständige OpenSpec-Artefakte allein SHALL nicht als Implementierungsnachweis gelten. + +#### Scenario: Abschlussbericht +- **WHEN** Phase 5 abgenommen wird +- **THEN** verweisen alle Phase-5-Checkboxen auf bestandene Nachweise und der Bericht unterscheidet die lokale headless Abnahme von der weiterhin separat geplanten Plattformmatrix aus Phase 6 + +### Requirement: Export-UI als eigene Phasenleistung +Die Phase-5-Abnahme SHALL die vollständige Bedienung von Make EXE File und Make Library einschließlich Zielsystem/-architektur, Ausgabepfad, Validierung, Überschreibentscheidung sowie Status-/Fehler-/Abbruchdarstellung prüfen. Dabei SHALL die noch nicht verfügbare native Erzeugung aus Phase 6 ausdrücklich erkennbar sein. Die Abnahme SHALL Oberflächennachweise von der späteren tatsächlichen Erzeugung eigenständiger nativer Executables und nativer Systembibliotheken unterscheiden. + +#### Scenario: Exportvorbereitung ohne Scheinerfolg +- **WHEN** beide Exportdialoge im durchgängigen IDE-Ablauf mit gültigen und ungültigen Angaben bedient werden +- **THEN** funktionieren Eingabe, Validierung und Rückkehr zum Projekt, die Erzeugungsaktion nennt die Phase-6-Abhängigkeit und weder eine falsche Erfolgsmeldung noch eine als natives Artefakt ausgegebene TBC-Datei entsteht