Implement and archive Phase 5 help system

This commit is contained in:
2026-09-07 09:59:21 +02:00
parent e236306657
commit cc82830889
23 changed files with 1950 additions and 56 deletions

24
Cargo.lock generated
View File

@@ -328,6 +328,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.2" version = "1.2.2"
@@ -393,6 +399,17 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pulldown-cmark"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.13.1",
"memchr",
"unicase",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -611,6 +628,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"crossterm", "crossterm",
"pulldown-cmark",
"ratatui", "ratatui",
"tb-frontend", "tb-frontend",
"tb-runtime", "tb-runtime",
@@ -676,6 +694,12 @@ dependencies = [
"syn 3.0.4", "syn 3.0.4",
] ]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"

View File

@@ -19,3 +19,4 @@ ratatui.workspace = true
crossterm.workspace = true crossterm.workspace = true
anyhow.workspace = true anyhow.workspace = true
unicode-width.workspace = true unicode-width.workspace = true
pulldown-cmark = { version = "0.13", default-features = false }

View File

@@ -278,6 +278,7 @@ pub enum Hit {
} }
pub struct App { pub struct App {
pub help: crate::help::Help,
pub debugger: crate::debugger::Debugger, pub debugger: crate::debugger::Debugger,
pub designer: crate::designer::Designer, pub designer: crate::designer::Designer,
pub session: crate::execution::Session, pub session: crate::execution::Session,
@@ -318,6 +319,7 @@ impl App {
let (options, errors, config_disk) = Options::load(&config_path); let (options, errors, config_disk) = Options::load(&config_path);
project.include_paths = options.include_paths.clone(); project.include_paths = options.include_paths.clone();
let mut app = Self { let mut app = Self {
help: Default::default(),
debugger: Default::default(), debugger: Default::default(),
designer: Default::default(), designer: Default::default(),
session: Default::default(), session: Default::default(),
@@ -480,9 +482,6 @@ impl App {
.collect() .collect()
} }
pub fn availability(&self, command: Command) -> Option<String> { pub fn availability(&self, command: Command) -> Option<String> {
if let Some(phase) = command.feature_phase() {
return Some(format!("Fachfunktion folgt in Phase-5-Change {phase:02}"));
}
use Command::*; use Command::*;
if command == Shell && self.session.host.shell_request.is_some() { if command == Shell && self.session.host.shell_request.is_some() {
return Some("Shell-Übergabe bereits angefordert".into()); return Some("Shell-Übergabe bereits angefordert".into());
@@ -575,6 +574,9 @@ impl App {
} }
} }
fn action(&mut self, command: Command) -> Result<()> { fn action(&mut self, command: Command) -> Result<()> {
if self.help_command(command)? {
return Ok(());
}
if self.debug_command(command)? { if self.debug_command(command)? {
return Ok(()); return Ok(());
} }
@@ -1432,6 +1434,40 @@ impl App {
} }
self.key(key); self.key(key);
} else if let Event::Mouse(mouse) = event { } else if let Event::Mouse(mouse) = event {
if mouse.kind == MouseEventKind::Down(MouseButton::Right)
&& self.options.right_help
&& !self.program_focus()
&& self.dialog.is_none()
{
if let Some((m, i)) = self.menu {
if let Some((_, Some(command))) = self.menu_entries(m).get(i) {
self.help_menu_context(*command);
}
} else {
self.execute(Command::Topic);
}
return;
}
if self.dialog.is_none()
&& self.menu.is_none()
&& self
.active_window()
.is_some_and(|w| w.kind == WindowKind::Help)
{
let delta = match mouse.kind {
MouseEventKind::ScrollDown => 1,
MouseEventKind::ScrollUp => -1,
_ => 0,
};
if delta != 0 {
let width = self
.rect(self.active_window().unwrap())
.width
.saturating_sub(2);
self.help.scroll(delta, width as usize);
return;
}
}
match self.design_mouse(mouse) { match self.design_mouse(mouse) {
Ok(true) => return, Ok(true) => return,
Err(e) => { Err(e) => {
@@ -1622,6 +1658,26 @@ impl App {
)) ))
} }
fn key(&mut self, key: KeyEvent) { fn key(&mut self, key: KeyEvent) {
if self.dialog.is_none() && key.code == K::F(1) {
if key.modifiers.is_empty() {
if let Some((m, i)) = self.menu {
if let Some((_, Some(command))) = self.menu_entries(m).get(i) {
self.help_menu_context(*command);
return;
}
}
}
if self.menu.is_none() {
if !self.help_key(key) {
self.execute(if key.modifiers.contains(M::SHIFT) {
Command::UsingHelp
} else {
Command::Topic
});
}
return;
}
}
if self.session.fullscreen && self.dialog.is_none() && key.code == K::F(4) { if self.session.fullscreen && self.dialog.is_none() && key.code == K::F(4) {
self.execute(Command::OutputScreen); self.execute(Command::OutputScreen);
return; return;
@@ -1758,6 +1814,9 @@ impl App {
} }
return; return;
} }
if self.help_key(key) {
return;
}
if key.code == K::F(11) if key.code == K::F(11)
|| matches!( || matches!(
key.code, key.code,

View File

@@ -97,15 +97,6 @@ pub enum Command {
ControlMenu, ControlMenu,
FocusWindow(u64), FocusWindow(u64),
} }
impl Command {
pub fn feature_phase(self) -> Option<u8> {
use Command::*;
match self {
HelpIndex | HelpContents | Keyboard | Topic | UsingHelp | Tutorial => Some(7),
_ => None,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Item { pub struct Item {
/// & markiert das sichtbare Mnemonic, … einen Dialog. /// & markiert das sichtbare Mnemonic, … einen Dialog.

View File

@@ -1104,6 +1104,16 @@ impl App {
} }
Ok(true) Ok(true)
} }
pub(crate) fn design_help_context(&mut self) -> Result<(String, String)> {
let node = self.selected_node()?;
let specs = forms::properties(node.class);
let property = if self.properties || self.value_focus {
specs.get(self.designer.property).map_or("", |p| p.name)
} else {
""
};
Ok((node.class.name().to_owned(), property.into()))
}
pub(crate) fn design_value_focus(&mut self) -> Result<()> { pub(crate) fn design_value_focus(&mut self) -> Result<()> {
let node = self.selected_node()?; let node = self.selected_node()?;
let specs = forms::properties(node.class); let specs = forms::properties(node.class);

972
crates/tb-ide/src/help.rs Normal file
View File

@@ -0,0 +1,972 @@
//! Embedded Markdown documentation. No filesystem, process or network access.
use crate::{
app::{App, Mode, WindowKind},
commands::Command,
};
use anyhow::{anyhow, ensure, Result};
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use std::{collections::BTreeSet, sync::OnceLock};
use unicode_width::UnicodeWidthChar;
const DOCUMENTS: &[(&str, &str)] = &[
("docs/hilfe.md", include_str!("../../../docs/hilfe.md")),
(
"docs/tutorial.md",
include_str!("../../../docs/tutorial.md"),
),
(
"docs/tastatur.md",
include_str!("../../../docs/tastatur.md"),
),
(
"docs/ide-bedienung.md",
include_str!("../../../docs/ide-bedienung.md"),
),
(
"docs/sprachreferenz.md",
include_str!("../../../docs/sprachreferenz.md"),
),
(
"docs/bibliothek.md",
include_str!("../../../docs/bibliothek.md"),
),
(
"docs/forms-referenz.md",
include_str!("../../../docs/forms-referenz.md"),
),
(
"docs/dateiformate.md",
include_str!("../../../docs/dateiformate.md"),
),
(
"docs/inventar.md",
include_str!("../../../docs/inventar.md"),
),
(
"docs/ide-referenz.md",
include_str!("../../../docs/ide-referenz.md"),
),
(
"docs/tbvm-design.md",
include_str!("../../../docs/tbvm-design.md"),
),
("PLAN.md", include_str!("../../../PLAN.md")),
];
#[derive(Clone, Debug)]
pub struct Glyph {
pub ch: char,
pub style: Style,
pub link: Option<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Block {
pub glyphs: Vec<Glyph>,
pub pre: bool,
}
impl Block {
pub fn text(&self) -> String {
self.glyphs.iter().map(|g| g.ch).collect()
}
}
#[derive(Clone, Debug)]
pub struct Link {
pub label: String,
pub target: String,
}
#[derive(Clone, Debug)]
pub struct Heading {
pub title: String,
pub anchor: String,
pub block: usize,
pub level: usize,
}
#[derive(Clone, Debug)]
pub struct Page {
pub path: String,
pub title: String,
pub blocks: Vec<Block>,
pub links: Vec<Link>,
pub headings: Vec<Heading>,
}
pub fn slug(text: &str) -> String {
text.to_lowercase()
.chars()
.filter_map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
Some(c)
} else if c.is_whitespace() {
Some('-')
} else {
None
}
})
.collect()
}
pub fn normalize(text: &str) -> String {
text.trim()
.trim_end_matches(['$', '%', '&', '!', '#'])
.to_uppercase()
}
fn words(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric() && !matches!(c, '_' | '$' | '%' | '&' | '!' | '#'))
.filter(|s| !s.is_empty())
.map(normalize)
.collect()
}
fn append(block: &mut Block, text: &str, style: Style, link: Option<usize>) {
block
.glyphs
.extend(text.chars().map(|ch| Glyph { ch, style, link }));
}
fn flush(blocks: &mut Vec<Block>, block: &mut Block) {
if !block.glyphs.is_empty() {
blocks.push(std::mem::take(block));
}
}
impl Page {
pub fn parse(path: &str, markdown: &str) -> Self {
let mut page = Self {
path: path.into(),
title: path.into(),
blocks: vec![],
links: vec![],
headings: vec![],
};
let mut block = Block::default();
let mut pre = false;
let mut heading = None;
let mut link = None;
let mut strong = 0usize;
let mut emphasis = 0usize;
let mut lists = Vec::<Option<u64>>::new();
let mut anchors = BTreeSet::new();
for event in Parser::new_ext(markdown, Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS) {
let mut style = Style::default();
if strong > 0 || heading.is_some() {
style = style.add_modifier(Modifier::BOLD);
}
if emphasis > 0 {
style = style.add_modifier(Modifier::ITALIC);
}
if heading.is_some() {
style = style.fg(Color::Yellow);
}
match event {
Event::Start(Tag::Heading { level, .. }) => {
flush(&mut page.blocks, &mut block);
heading = Some(level as usize);
}
Event::End(TagEnd::Heading(_)) => {
let title = block.text();
let base = slug(&title);
let mut anchor = base.clone();
let mut suffix = 0;
while !anchors.insert(anchor.clone()) {
suffix += 1;
anchor = format!("{base}-{suffix}");
}
if page.headings.is_empty() {
page.title = title.clone();
}
page.headings.push(Heading {
title,
anchor,
block: page.blocks.len(),
level: heading.take().unwrap(),
});
flush(&mut page.blocks, &mut block);
}
Event::Start(Tag::CodeBlock(_)) => {
flush(&mut page.blocks, &mut block);
pre = true;
block.pre = true;
}
Event::End(TagEnd::CodeBlock) => {
flush(&mut page.blocks, &mut block);
pre = false;
}
Event::Start(Tag::Table(_)) => {
flush(&mut page.blocks, &mut block);
pre = true;
block.pre = true;
}
Event::End(TagEnd::Table) => {
flush(&mut page.blocks, &mut block);
pre = false;
}
Event::End(TagEnd::TableHead | TagEnd::TableRow) => {
flush(&mut page.blocks, &mut block);
block.pre = true;
}
Event::Start(Tag::TableCell) => append(&mut block, " | ", style, None),
Event::End(TagEnd::TableCell) => {}
Event::Start(Tag::List(start)) => {
flush(&mut page.blocks, &mut block);
lists.push(start);
}
Event::End(TagEnd::List(_)) => {
flush(&mut page.blocks, &mut block);
lists.pop();
}
Event::Start(Tag::Item) => {
flush(&mut page.blocks, &mut block);
let prefix = match lists.last_mut() {
Some(Some(n)) => {
let p = format!("{n}. ");
*n += 1;
p
}
_ => "".into(),
};
append(
&mut block,
&format!("{}{prefix}", " ".repeat(lists.len().saturating_sub(1))),
style,
None,
);
}
Event::End(TagEnd::Item | TagEnd::Paragraph) => flush(&mut page.blocks, &mut block),
Event::Start(Tag::Strong) => strong += 1,
Event::End(TagEnd::Strong) => strong = strong.saturating_sub(1),
Event::Start(Tag::Emphasis) => emphasis += 1,
Event::End(TagEnd::Emphasis) => emphasis = emphasis.saturating_sub(1),
Event::Start(Tag::Link { dest_url, .. } | Tag::Image { dest_url, .. }) => {
link = Some(page.links.len());
page.links.push(Link {
label: String::new(),
target: dest_url.into_string(),
});
append(&mut block, "", style, link);
}
Event::End(TagEnd::Link | TagEnd::Image) => {
append(&mut block, "", style, link);
if let Some(id) = link.take() {
if external(&page.links[id].target) {
append(
&mut block,
&format!(" [extern: {}]", page.links[id].target),
style,
Some(id),
);
}
}
}
Event::Text(text)
| Event::Code(text)
| Event::InlineHtml(text)
| Event::Html(text) => {
if let Some(id) = link {
page.links[id].label.push_str(&text);
}
if pre {
for (n, line) in text.split('\n').enumerate() {
if n > 0 {
page.blocks.push(std::mem::take(&mut block));
block.pre = true;
}
append(&mut block, &line.replace('\t', " "), style, link);
}
} else {
append(&mut block, &text, style, link);
}
}
Event::SoftBreak => append(&mut block, " ", style, link),
Event::HardBreak => {
flush(&mut page.blocks, &mut block);
block.pre = pre;
}
Event::Rule => {
flush(&mut page.blocks, &mut block);
append(&mut block, "────────", style, None);
flush(&mut page.blocks, &mut block);
}
Event::TaskListMarker(checked) => append(
&mut block,
if checked { "[x] " } else { "[ ] " },
style,
None,
),
_ => {}
}
}
flush(&mut page.blocks, &mut block);
page
}
}
pub fn external(target: &str) -> bool {
target.contains(':') || target.starts_with("//")
}
fn decode(text: &str) -> Result<String> {
let mut bytes = vec![];
let mut iter = text.bytes();
while let Some(c) = iter.next() {
if c == b'%' {
let hi = iter.next().and_then(|c| (c as char).to_digit(16));
let lo = iter.next().and_then(|c| (c as char).to_digit(16));
bytes.push(
(hi.ok_or_else(|| anyhow!("Ungültige URL-Kodierung"))? * 16
+ lo.ok_or_else(|| anyhow!("Ungültige URL-Kodierung"))?) as u8,
);
} else {
bytes.push(c);
}
}
Ok(String::from_utf8(bytes)?)
}
#[derive(Debug)]
pub struct Catalog {
pub pages: Vec<Page>,
}
impl Catalog {
pub fn embedded() -> &'static Self {
static CATALOG: OnceLock<Catalog> = OnceLock::new();
CATALOG.get_or_init(|| Self::new(DOCUMENTS))
}
pub fn new(documents: &[(&str, &str)]) -> Self {
let mut pages: Vec<_> = documents.iter().map(|(p, md)| Page::parse(p, md)).collect();
let mut contents = String::from("# Contents\n\n");
let mut entries = vec![];
for p in &pages {
contents.push_str(&format!("- [{}]({})\n", p.title, p.path));
entries.push((p.title.clone(), p.path.clone()));
for h in p.headings.iter().skip(1) {
let target = format!("{}#{}", p.path, h.anchor);
contents.push_str(&format!(
"{}- [{}]({target})\n",
" ".repeat(h.level.saturating_sub(1)),
h.title
));
entries.push((format!("{}{}", h.title, p.title), target));
}
}
entries.sort_by_key(|(label, _)| label.to_lowercase());
let index = format!(
"# Index\n\n{}",
entries
.into_iter()
.map(|(l, t)| format!("- [{l}]({t})\n"))
.collect::<String>()
);
pages.push(Page::parse("contents", &contents));
pages.push(Page::parse("index", &index));
Self { pages }
}
pub fn resolve(&self, source: &str, target: &str) -> Result<(usize, usize)> {
ensure!(
!external(target),
"Externes Ziel: {target} — wird nicht automatisch geöffnet"
);
let (path, anchor) = target.split_once('#').unwrap_or((target, ""));
let path = decode(path)?;
ensure!(
!path.starts_with('/') && !path.contains('\\') && !path.contains(':'),
"Ziel außerhalb des Hilfekatalogs"
);
let mut parts: Vec<&str> = if path.is_empty() {
source.split('/').collect()
} else {
source
.rsplit_once('/')
.map(|(p, _)| p.split('/').collect())
.unwrap_or_default()
};
for part in path.split('/').filter(|s| !s.is_empty() && *s != ".") {
if part == ".." {
ensure!(parts.pop().is_some(), "Ziel außerhalb des Hilfekatalogs");
} else {
parts.push(part);
}
}
let path = parts.join("/");
let page = self
.pages
.iter()
.position(|p| p.path == path)
.ok_or_else(|| anyhow!("Hilfedatei fehlt: {path}"))?;
let anchor = decode(anchor)?;
let block = if anchor.is_empty() {
0
} else {
self.pages[page]
.headings
.iter()
.find(|h| h.anchor == anchor)
.ok_or_else(|| anyhow!("Hilfeanker fehlt: {path}#{anchor}"))?
.block
};
Ok((page, block))
}
pub fn validate(&self) -> Result<()> {
for page in &self.pages {
for link in &page.links {
if !external(&link.target) {
self.resolve(&page.path, &link.target)?;
}
}
}
Ok(())
}
pub fn search(&self, query: &str, only: Option<&str>) -> Vec<(usize, usize)> {
let query = normalize(query);
if query.is_empty() {
return vec![];
}
let mut result = vec![];
for (id, page) in
self.pages.iter().enumerate().filter(|(_, p)| {
p.path.starts_with("docs/") && only.is_none_or(|path| p.path == path)
})
{
for h in &page.headings {
let end = page
.headings
.iter()
.find(|next| next.block > h.block)
.map_or(page.blocks.len(), |next| next.block);
if page.blocks[h.block..end]
.iter()
.any(|b| words(&b.text()).contains(&query))
{
result.push((id, h.block));
}
}
}
result
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Position {
pub page: usize,
pub block: usize,
pub offset: usize,
pub link: Option<usize>,
pub horizontal: usize,
pub query: Option<String>,
}
#[derive(Default)]
pub struct Help {
pub position: Position,
pub history: Vec<Position>,
pub input: String,
pub notice: String,
pub return_focus: Option<(u64, bool, bool, bool)>,
}
#[derive(Debug)]
pub struct Row {
pub block: usize,
pub start: usize,
pub end: usize,
}
pub fn layout(page: &Page, width: usize) -> Vec<Row> {
let width = width.max(1);
let mut rows = vec![];
for (index, block) in page.blocks.iter().enumerate() {
if block.pre || block.glyphs.is_empty() {
rows.push(Row {
block: index,
start: 0,
end: block.glyphs.len(),
});
continue;
}
let mut start = 0;
while start < block.glyphs.len() {
let mut end = start;
let mut cells = 0;
let mut space = None;
while end < block.glyphs.len() {
let ch = block.glyphs[end].ch;
let w = ch.width().unwrap_or(0);
if cells + w > width && end > start {
break;
}
cells += w;
end += 1;
if ch.is_whitespace() {
space = Some(end);
}
}
if end < block.glyphs.len() {
if let Some(s) = space {
end = s;
}
}
rows.push(Row {
block: index,
start,
end,
});
start = end;
}
rows.push(Row {
block: index,
start: block.glyphs.len(),
end: block.glyphs.len(),
});
}
rows
}
impl Help {
pub fn page(&self) -> std::borrow::Cow<'_, Page> {
let catalog = Catalog::embedded();
if let Some(query) = &self.position.query {
let matches = catalog.search(query, None);
let mut md = format!("# Index: {}\n\n", query.replace(['[', ']', '<', '>'], ""));
if matches.is_empty() {
md.push_str("Kein passendes Thema. Suche ändern oder im Index wählen.\n\n");
}
for (p, b) in matches {
let page = &catalog.pages[p];
let h = page.headings.iter().find(|h| h.block == b).unwrap();
md.push_str(&format!(
"- [{}{}]({}#{})\n",
h.title, page.title, page.path, h.anchor
));
}
md.push_str("\n[Alphabetischer Index](index) · [Contents](contents)\n");
std::borrow::Cow::Owned(Page::parse("search", &md))
} else {
std::borrow::Cow::Borrowed(&catalog.pages[self.position.page])
}
}
pub fn visit(&mut self, page: usize, block: usize, query: Option<String>) {
if self.history.len() == 20 {
self.history.remove(0);
}
self.history.push(self.position.clone());
self.position = Position {
page,
block,
query,
..Default::default()
};
self.notice.clear();
self.input.clear();
}
pub fn open(&mut self, target: &str) -> Result<()> {
let source = self.page().path.clone();
let (page, block) = Catalog::embedded().resolve(&source, target)?;
self.visit(page, block, None);
Ok(())
}
pub fn follow(&mut self) -> Result<()> {
let target = self
.page()
.links
.get(
self.position
.link
.ok_or_else(|| anyhow!("Zuerst mit Tab einen Link wählen"))?,
)
.ok_or_else(|| anyhow!("Link fehlt"))?
.target
.clone();
self.open(&target)
}
pub fn back(&mut self) {
if let Some(p) = self.history.pop() {
self.position = p;
self.notice.clear();
self.input.clear();
} else {
self.notice = "Keine ältere Hilfeseite; maximal 20 Rücksprünge".into();
}
}
pub fn next(&mut self) {
let end = DOCUMENTS.len();
if self.position.page + 1 < end && self.position.query.is_none() {
self.visit(self.position.page + 1, 0, None);
} else if self.position.page >= end || self.position.query.is_some() {
self.visit(0, 0, None);
} else {
self.notice = "Ende der Contents-Themenreihenfolge".into();
}
}
pub fn focus_link(&mut self, backwards: bool) {
let page = self.page();
let n = page.links.len();
if n == 0 {
return;
}
let id = match self.position.link {
Some(i) => (i + n + if backwards { n - 1 } else { 1 }) % n,
None => {
if backwards {
n - 1
} else {
0
}
}
};
let location = page.blocks.iter().enumerate().find_map(|(b, block)| {
block
.glyphs
.iter()
.position(|g| g.link == Some(id))
.map(|offset| (b, offset, block.pre))
});
self.position.link = Some(id);
if let Some((b, o, pre)) = location {
self.position.block = b;
self.position.offset = o;
self.position.horizontal = if pre {
self.page().blocks[b].glyphs[..o]
.iter()
.map(|g| g.ch.width().unwrap_or(0))
.sum()
} else {
0
};
}
}
pub fn scroll(&mut self, delta: isize, width: usize) {
let rows = layout(&self.page(), width);
if rows.is_empty() {
return;
}
let current = self.row(&rows);
let index = current.saturating_add_signed(delta).min(rows.len() - 1);
self.position.block = rows[index].block;
self.position.offset = rows[index].start;
}
fn row(&self, rows: &[Row]) -> usize {
rows.iter()
.position(|r| {
r.block == self.position.block
&& r.start <= self.position.offset
&& (self.position.offset < r.end || r.start == r.end)
})
.unwrap_or_else(|| {
rows.iter()
.position(|r| r.block >= self.position.block)
.unwrap_or(0)
})
}
}
impl App {
fn help_show(&mut self) {
if self
.active_window()
.is_none_or(|w| w.kind != WindowKind::Help)
{
self.help.return_focus = Some((
self.active,
self.properties,
self.value_focus,
self.session.fullscreen,
));
}
self.properties = false;
self.value_focus = false;
self.session.fullscreen = false;
self.show_tool(WindowKind::Help);
}
pub(crate) fn help_close(&mut self) {
self.windows.retain(|w| w.kind != WindowKind::Help);
if let Some((id, p, v, fullscreen)) = self.help.return_focus.take() {
self.active = if self.windows.iter().any(|w| w.id == id) {
id
} else {
self.windows.first().map_or(0, |w| w.id)
};
self.properties = p;
self.value_focus = v;
self.session.fullscreen = fullscreen;
}
}
pub fn help_context(&mut self, token: &str, class: Option<&str>) {
let catalog = Catalog::embedded();
let matches = if let Some(class) = class {
let page = catalog
.pages
.iter()
.position(|p| p.path == "docs/forms-referenz.md")
.unwrap();
let p = &catalog.pages[page];
let heading = p
.headings
.iter()
.find(|h| words(&h.title).contains(&normalize(class)));
heading
.map(|h| {
let end = p
.headings
.iter()
.find(|next| next.block > h.block)
.map_or(p.blocks.len(), |next| next.block);
let block = (h.block..end)
.find(|b| words(&p.blocks[*b].text()).contains(&normalize(token)))
.unwrap_or(h.block);
vec![(page, block)]
})
.unwrap_or_default()
} else {
let library = catalog.search(token, Some("docs/bibliothek.md"));
if !library.is_empty() {
library
} else {
catalog.search(token, None)
}
};
self.help_show();
if matches.len() == 1 {
self.help.visit(matches[0].0, matches[0].1, None);
} else if token.is_empty() {
self.help.visit(catalog.pages.len() - 1, 0, None);
} else {
self.help
.visit(catalog.pages.len() - 1, 0, Some(token.into()));
}
}
pub(crate) fn help_command(&mut self, command: Command) -> Result<bool> {
use Command::*;
let target = match command {
HelpIndex => "index",
HelpContents => "contents",
Keyboard => "docs/tastatur.md",
UsingHelp => "docs/hilfe.md",
Tutorial => "docs/tutorial.md",
HelpWindow => {
self.help_show();
return Ok(true);
}
Topic => {
if self
.active_window()
.is_some_and(|w| w.kind == WindowKind::Help)
{
self.help_context("", None);
return Ok(true);
}
if self.mode == Mode::Designer {
if let Ok((class, property)) = self.design_help_context() {
self.help_context(&property, Some(&class));
return Ok(true);
}
}
let token = self
.editor_view()
.ok()
.and_then(|v| self.project.view(v).ok())
.and_then(|v| {
self.project.document(v.document()).ok().map(|d| {
token_at(
self.editor
.expansions
.get(&self.editor_view().unwrap())
.map_or(d.code(), String::as_str),
v.cursor,
)
})
})
.unwrap_or_default();
self.help_context(&token, None);
return Ok(true);
}
CloseWindow
if self
.active_window()
.is_some_and(|w| w.kind == WindowKind::Help) =>
{
self.help_close();
return Ok(true);
}
_ => return Ok(false),
};
let (page, block) = Catalog::embedded().resolve("", target)?;
self.help_show();
self.help.visit(page, block, None);
Ok(true)
}
pub(crate) fn help_menu_context(&mut self, command: Command) {
let target = command_target(command, self.mode == Mode::Designer);
self.menu = None;
self.control_menu = false;
self.help_show();
match Catalog::embedded().resolve("", target) {
Ok((p, b)) => self.help.visit(p, b, None),
Err(e) => self.help.notice = e.to_string(),
}
}
pub(crate) fn help_key(&mut self, key: KeyEvent) -> bool {
if self
.active_window()
.is_none_or(|w| w.kind != WindowKind::Help)
{
return false;
}
let r = self.rect(self.active_window().unwrap());
let width = r.width.saturating_sub(2) as usize;
let page = r.height.saturating_sub(4).max(1) as isize;
match key.code {
K::Esc => self.help_close(),
K::F(1) if key.modifiers.contains(M::ALT) => self.help.back(),
K::F(1) if key.modifiers.contains(M::CONTROL) => self.help.next(),
K::Tab => self.help.focus_link(key.modifiers.contains(M::SHIFT)),
K::BackTab => self.help.focus_link(true),
K::Enter => {
if self.help.input.is_empty() {
if let Err(e) = self.help.follow() {
self.help.notice = e.to_string();
}
} else {
let query = std::mem::take(&mut self.help.input);
self.help
.visit(Catalog::embedded().pages.len() - 1, 0, Some(query));
}
}
K::Up => self.help.scroll(-1, width),
K::Down => self.help.scroll(1, width),
K::PageUp => self.help.scroll(-page, width),
K::PageDown => self.help.scroll(page, width),
K::Home => {
self.help.position.block = 0;
self.help.position.offset = 0;
}
K::End => self.help.scroll(isize::MAX, width),
K::Left if !key.modifiers.contains(M::CONTROL) => {
self.help.position.horizontal = self.help.position.horizontal.saturating_sub(4)
}
K::Right if !key.modifiers.contains(M::CONTROL) => {
self.help.position.horizontal = self.help.position.horizontal.saturating_add(4)
}
K::Backspace => {
self.help.input.pop();
}
K::Char(c) if !key.modifiers.intersects(M::ALT | M::CONTROL) => self.help.input.push(c),
K::Delete => {}
_ => return false,
}
true
}
pub(crate) fn help_render(&self, f: &mut Frame, area: Rect) {
if area.width == 0 || area.height == 0 {
return;
}
let page = self.help.page();
let rows = layout(&page, area.width as usize);
let start = self.help.row(&rows);
f.render_widget(
Paragraph::new(format!("{} · Suche: {}", page.title, self.help.input))
.style(Style::default().fg(Color::Yellow)),
Rect::new(area.x, area.y, area.width, 1),
);
for (y, row) in rows
.iter()
.skip(start)
.take(area.height.saturating_sub(2) as usize)
.enumerate()
{
let block = &page.blocks[row.block];
let mut x = 0;
let shift = if block.pre {
self.help.position.horizontal
} else {
0
};
let mut spans: Vec<Span> = vec![];
for g in &block.glyphs[row.start..row.end] {
let w = g.ch.width().unwrap_or(0);
let end = x + w;
if x >= shift && end <= shift + area.width as usize {
let mut style = g.style;
if let Some(link) = g.link {
style = style.fg(Color::Cyan).add_modifier(Modifier::UNDERLINED);
if self.help.position.link == Some(link) {
style = style.bg(Color::Blue).fg(Color::White);
}
}
if let Some(last) = spans.last_mut().filter(|s| s.style == style) {
last.content.to_mut().push(g.ch);
} else {
spans.push(Span::styled(g.ch.to_string(), style));
}
} else if x < shift && end > shift {
spans.push(Span::raw(" ".repeat(end - shift)));
}
x = end;
}
f.render_widget(
Paragraph::new(Line::from(spans)),
Rect::new(area.x, area.y + 1 + y as u16, area.width, 1),
);
}
if area.height > 1 {
let status = if self.help.notice.is_empty() {
"Tab: Link · Enter: folgen · Alt+F1: zurück · Esc: schließen"
} else {
&self.help.notice
};
f.render_widget(
Paragraph::new(status),
Rect::new(area.x, area.bottom() - 1, area.width, 1),
);
}
}
}
pub fn token_at(code: &str, cursor: usize) -> String {
let mut cursor = cursor.min(code.len());
while !code.is_char_boundary(cursor) {
cursor -= 1;
}
let valid = |c: char| c.is_alphanumeric() || matches!(c, '_' | '$' | '%' | '&' | '!' | '#');
let start = code[..cursor]
.char_indices()
.rev()
.take_while(|(_, c)| valid(*c))
.last()
.map_or(cursor, |(i, _)| i);
let end = code[cursor..]
.char_indices()
.find(|(_, c)| !valid(*c))
.map_or(code.len(), |(i, _)| cursor + i);
code[start..end].into()
}
/// Context aliases for concrete command IDs; every target is checked with the catalog.
pub fn command_target(command: Command, designer: bool) -> &'static str {
use Command::*;
match command {
MakeExe | MakeLibrary => {
"docs/ide-bedienung.md#native-exporte-ui-in-phase-5-erzeugung-in-phase-6"
}
NewForm | Form | Events | MenuBar | Grid | Palette | MenuDesign | Toolbox | Tool(_) => {
"docs/ide-bedienung.md#formulare-gestalten"
}
Undo | Cut | Copy | Paste | Clear if designer => {
"docs/ide-bedienung.md#formulare-gestalten"
}
Undo | Cut | Copy | Paste | Clear | Procedures | PreviousCode | NewSub | NewFunction
| Code | IncludedFile | IncludedLines | Find | SelectedText | FindNext | Replace
| LoadText | SaveText => "docs/ide-bedienung.md#editor-suche-und-quelltextpflege",
Diagnostics | SyntaxChecking => "docs/ide-bedienung.md#übersetzung-und-revisionsbindung",
Start | Restart | Continue | Pause | CommandLine | Startup | Output | OutputScreen
| Shell => "docs/ide-bedienung.md#programme-ausführen",
AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches | Trace | History
| Breakpoint | ClearBreakpoints | BreakErrors | SetStatement | RunToCursor | Step
| ProcedureStep | HistoryBack | HistoryForward | NextStatement | Calls | Debug
| Immediate => "docs/ide-bedienung.md#debugger-und-direktfenster",
Display | Paths | SaveOptions => "docs/ide-bedienung.md#display-und-benutzereinstellungen",
RightMouse | Topic | UsingHelp | HelpIndex | HelpContents | HelpWindow | About => {
"docs/hilfe.md"
}
Keyboard => "docs/tastatur.md",
Tutorial | NewProject | NewModule => "docs/tutorial.md#projekt-und-formular-erstellen",
OpenProject | SaveProject | AddFile | RemoveFile | SaveFile | SaveAs | Exit => {
"docs/ide-bedienung.md#speichern-und-wechseln"
}
Print => "docs/ide-bedienung.md#programme-ausführen",
Project | NewWindow | Arrange | NextWindow | PreviousWindow | CloseWindow | MoveWindow
| SizeWindow | Minimize | Maximize | Restore | ControlMenu | FocusWindow(_) => {
"docs/ide-bedienung.md#menüs-fenster-und-dialoge"
}
}
}

View File

@@ -14,3 +14,5 @@ pub mod execution;
pub mod designer; pub mod designer;
pub mod debugger; pub mod debugger;
pub mod help;

View File

@@ -413,15 +413,7 @@ impl App {
kind @ (WindowKind::Calls | WindowKind::Debug | WindowKind::Immediate) => { kind @ (WindowKind::Calls | WindowKind::Debug | WindowKind::Immediate) => {
self.debug_render(f, kind, inner) self.debug_render(f, kind, inner)
} }
kind => put( WindowKind::Help => self.help_render(f, inner),
f,
inner,
match kind {
WindowKind::Help => "Help · Inhalte folgen in Change 07",
_ => "Debugger-Inhalte folgen in Change 06",
},
content_style,
),
} }
} }
if self.mode == Mode::Environment { if self.mode == Mode::Environment {

529
crates/tb-ide/tests/help.rs Normal file
View File

@@ -0,0 +1,529 @@
use crossterm::event::{
Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
app::{App, Mode, WindowKind},
commands::Command,
help::{layout, normalize, token_at, Catalog, Help, Page},
};
struct Temp(PathBuf);
impl Temp {
fn new() -> Self {
static N: AtomicUsize = AtomicUsize::new(0);
let p = std::env::temp_dir().join(format!(
"tb-help-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&p).unwrap();
Self(p.canonicalize().unwrap())
}
fn app(&self) -> App {
App::new(&self.0, self.0.join("options"), (100, 30)).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 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::<String>())
.collect::<Vec<_>>()
.join("\n")
}
fn menu(a: &mut App, c: Command) {
let (m, i) = a
.menus()
.iter()
.enumerate()
.find_map(|(m, menu)| {
menu.items
.iter()
.position(|i| i.command == Some(c))
.map(|i| (m, i))
})
.unwrap();
a.menu = Some((m, i));
key(a, K::Enter, M::NONE);
assert_eq!(a.last_command, Some(c));
}
#[test]
fn catalog_links_and_missing_targets_are_checked() {
let c = Catalog::embedded();
c.validate().unwrap();
for p in &c.pages {
for h in &p.headings {
let (_, b) = c.resolve("", &format!("{}#{}", p.path, h.anchor)).unwrap();
assert_eq!(b, h.block);
}
}
let duplicates = Catalog::new(&[("a.md", "# Titel\n\n## Titel\n\n## Titel-1\n\n## Titel\n")]);
for h in &duplicates.pages[0].headings {
assert_eq!(
duplicates
.resolve("", &format!("a.md#{}", h.anchor))
.unwrap()
.1,
h.block
);
}
let bad = Catalog::new(&[("docs/a.md", "# A\n\n[broken](missing.md)")]);
assert!(bad.validate().unwrap_err().to_string().contains("fehlt"));
let bad = Catalog::new(&[("docs/a.md", "# A\n\n[broken](#missing)")]);
assert!(bad.validate().is_err());
for target in [
"../../secret",
"%2e%2e/%2e%2e/secret",
"/etc/passwd",
"file:///etc/passwd",
"../missing.md",
"hilfe.md#missing",
"hilfe.md#%GG",
"C:%5csecret",
] {
assert!(c.resolve("docs/hilfe.md", target).is_err(), "{target}");
}
assert!(c.resolve("docs/hilfe.md", "../PLAN.md").is_ok());
let t = Temp::new();
let mut a = t.app();
a.execute(Command::UsingHelp);
let before = a.help.position.clone();
assert!(a
.help
.open("https://example.org/private")
.unwrap_err()
.to_string()
.contains("Extern"));
assert_eq!(a.help.position, before);
}
#[test]
fn markdown_blocks_escapes_links_and_unicode_layout() {
let p=Page::parse("sample","# Überschrift\n\nAbsatz mit **fett**, *kursiv*, \\*Escape\\* und [Link](#überschrift).\n\n1. Eins\n2. Zwei\n\n```basic\nPRINT \"界ä\"\n012345678901234567890123456789\n```\n\n| Spalte | Breit |\n|---|---|\n| Wert | 界界界界 |\n");
let text = p
.blocks
.iter()
.map(|b| b.text())
.collect::<Vec<_>>()
.join("\n");
assert!(text.contains("*Escape*"));
assert!(text.contains("◄Link►"));
assert!(text.contains("1. Eins"));
assert!(text.contains("2. Zwei"));
assert!(p.blocks.iter().any(|b| b.pre && b.text().contains("PRINT")));
assert!(p.blocks.iter().any(|b| b.pre && b.text().contains("界界")));
assert_eq!(p.headings[0].anchor, "überschrift");
for width in [1, 7, 20, 80] {
let rows = layout(&p, width);
for (i, b) in p.blocks.iter().enumerate() {
let reconstructed: String = rows
.iter()
.filter(|r| r.block == i)
.flat_map(|r| b.glyphs[r.start..r.end].iter().map(|g| g.ch))
.collect();
assert_eq!(reconstructed, b.text());
}
}
}
#[test]
fn offline_help_child() {
if std::env::var_os("TB_HELP_OFFLINE_CHILD").is_none() {
return;
}
let base = std::env::current_dir().unwrap();
assert!(!base.join("docs").exists());
let mut a = App::new(&base, base.join("options"), (100, 30)).unwrap();
for command in [
Command::HelpIndex,
Command::HelpContents,
Command::Keyboard,
Command::UsingHelp,
Command::Tutorial,
] {
a.execute(command);
assert_eq!(a.active_window().unwrap().kind, WindowKind::Help);
assert!(!draw(&mut a).is_empty());
}
for path in [
"docs/sprachreferenz.md",
"docs/bibliothek.md",
"docs/forms-referenz.md",
"docs/ide-bedienung.md",
"docs/tutorial.md",
] {
let (p, b) = Catalog::embedded().resolve("", path).unwrap();
a.help.visit(p, b, None);
assert!(a.help.page().blocks.len() > 10);
assert!(draw(&mut a).contains(&a.help.page().title));
}
}
#[test]
fn help_works_from_a_directory_without_checkout() {
let t = Temp::new();
let status = std::process::Command::new(std::env::current_exe().unwrap())
.current_dir(&t.0)
.env("TB_HELP_OFFLINE_CHILD", "1")
.args(["--exact", "offline_help_child", "--nocapture"])
.status()
.unwrap();
assert!(status.success());
}
#[test]
fn context_functions_properties_commands_and_unknown_queries() {
assert_eq!(normalize("lEfT$"), "LEFT");
assert_eq!(token_at("PRINT LEFT$(x$, 2)", 8), "LEFT$");
let t = Temp::new();
let mut a = t.app();
let id = a.active_document().unwrap();
a.project
.replace_text(id, 0..0, "PRINT lEfT$(\"abc\", 2)")
.unwrap();
let view = match a.active_window().unwrap().kind {
WindowKind::Code(v) => v,
_ => panic!(),
};
a.project.view_mut(view).unwrap().cursor = 9;
key(&mut a, K::F(1), M::NONE);
assert_eq!(a.help.page().path, "docs/bibliothek.md");
assert!(a.help.page().blocks[a.help.position.block]
.text()
.contains("Strings"));
key(&mut a, K::Esc, M::NONE);
a.execute(Command::NewForm);
assert_eq!(a.mode, Mode::Designer);
let specs = tb_frontend::forms::properties(tb_frontend::forms::ObjectClass::Form);
a.designer.property = specs.iter().position(|p| p.name == "CAPTION").unwrap();
a.properties = true;
let previous = a.active;
key(&mut a, K::F(2), M::NONE);
key(&mut a, K::F(1), M::NONE);
assert_eq!(a.help.page().path, "docs/forms-referenz.md");
assert!(a.help.page().blocks[a.help.position.block]
.text()
.contains("Caption"));
assert!(draw(&mut a).contains("Caption"));
key(&mut a, K::Esc, M::NONE);
assert_eq!(a.active, previous);
assert!(a.properties && a.value_focus);
a.value_focus = false;
a.properties = false;
let (m, i) = a
.menus()
.iter()
.enumerate()
.find_map(|(m, menu)| {
menu.items
.iter()
.position(|i| i.command == Some(Command::SaveProject))
.map(|i| (m, i))
})
.unwrap();
a.menu = Some((m, i));
key(&mut a, K::F(1), M::NONE);
assert!(a.help.page().blocks[a.help.position.block]
.text()
.contains("Speichern"));
a.help_context("unbekanntxyz", None);
assert!(draw(&mut a).contains("unbekanntxyz"));
assert!(draw(&mut a).contains("Kein passendes Thema"));
a.help_context("PRINT", None);
assert!(a.help.position.query.is_some());
assert!(a.help.page().links.len() > 2);
}
#[test]
fn links_history_resize_scroll_and_focus_use_real_events() {
let t = Temp::new();
let mut a = t.app();
let previous = a.active;
menu(&mut a, Command::HelpContents);
let help_id = a.active;
key(&mut a, K::Tab, M::NONE);
let first = a.help.position.link;
key(&mut a, K::Tab, M::NONE);
assert_ne!(a.help.position.link, first);
key(&mut a, K::BackTab, M::SHIFT);
assert_eq!(a.help.position.link, first);
key(&mut a, K::Down, M::NONE);
let before = a.help.position.clone();
key(&mut a, K::Enter, M::NONE);
assert_ne!(a.help.position.page, before.page);
key(&mut a, K::F(1), M::ALT);
assert_eq!(a.help.position, before);
let w = a.windows.iter_mut().find(|w| w.id == help_id).unwrap();
w.normal = Rect::new(2, 2, 28, 18);
let small = draw(&mut a);
assert!(small.contains("Contents"));
let pos = a.help.position.clone();
a.handle(Event::Resize(120, 40));
a.windows
.iter_mut()
.find(|w| w.id == help_id)
.unwrap()
.normal
.width = 85;
draw(&mut a);
assert_eq!(a.help.position, pos);
let (p, b) = Catalog::embedded().resolve("", "docs/tutorial.md").unwrap();
a.help.visit(p, b, None);
key(&mut a, K::PageDown, M::NONE);
assert!(a.help.position.block > 0);
key(&mut a, K::Home, M::NONE);
assert_eq!(a.help.position.block, 0);
key(&mut a, K::Esc, M::NONE);
assert_eq!(a.active, previous);
assert!(!a.windows.iter().any(|w| w.kind == WindowKind::Help));
}
#[test]
fn twenty_back_steps_end_of_contents_and_all_entries() {
let mut h = Help::default();
let mut states = vec![];
for i in 0..25 {
states.push(h.position.clone());
h.visit(i % 12, i, None);
}
assert_eq!(h.history.len(), 20);
for expected in states.into_iter().rev().take(20) {
h.back();
assert_eq!(h.position, expected);
}
let p = h.position.clone();
h.back();
assert_eq!(h.position, p);
assert!(h.notice.contains("20"));
h.visit(11, 0, None);
let p = h.position.clone();
h.next();
assert_eq!(h.position, p);
assert!(h.notice.contains("Ende"));
let t = Temp::new();
let mut a = t.app();
for (c, path) in [
(Command::HelpIndex, "index"),
(Command::HelpContents, "contents"),
(Command::Keyboard, "docs/tastatur.md"),
(Command::UsingHelp, "docs/hilfe.md"),
(Command::Tutorial, "docs/tutorial.md"),
] {
menu(&mut a, c);
assert_eq!(a.help.page().path, path);
assert!(draw(&mut a).contains(&a.help.page().title));
}
key(&mut a, K::F(1), M::SHIFT);
assert_eq!(a.help.page().path, "docs/hilfe.md");
key(&mut a, K::F(1), M::CONTROL);
assert_eq!(a.help.page().path, "docs/tutorial.md");
menu(&mut a, Command::About);
let title = &a.dialog.as_ref().unwrap().title;
assert!(title.contains(env!("CARGO_PKG_VERSION")));
assert!(title.contains(env!("CARGO_PKG_AUTHORS")));
assert!(title.contains("Copyright"));
}
#[test]
fn right_click_configuration_and_external_link_follow() {
let t = Temp::new();
let mut a = t.app();
let event = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Right),
column: 2,
row: 3,
modifiers: M::NONE,
});
a.options.right_help = false;
a.handle(event.clone());
assert_ne!(a.active_window().unwrap().kind, WindowKind::Help);
a.options.right_help = true;
a.handle(event);
assert_eq!(a.active_window().unwrap().kind, WindowKind::Help);
let (p, b) = Catalog::embedded()
.resolve("", "docs/dateiformate.md")
.unwrap();
a.help.visit(p, b, None);
let link = a
.help
.page()
.links
.iter()
.position(|l| l.target.starts_with("https:"))
.unwrap();
for _ in 0..=link {
key(&mut a, K::Tab, M::NONE);
}
assert!(draw(&mut a).contains("extern"));
let before = a.help.position.clone();
key(&mut a, K::Enter, M::NONE);
assert_eq!(a.help.position, before);
assert!(a.help.notice.contains("nicht automatisch"));
assert!(!a.session.file_shell);
assert!(a.session.host.shell_request.is_none());
}
#[test]
fn every_command_and_forms_alias_resolves_and_invalid_alias_fails() {
use tb_frontend::forms::ObjectClass;
let c = Catalog::embedded();
for designer in [false, true] {
for menu in tb_ide::commands::menus(designer) {
for command in menu.items.iter().filter_map(|i| i.command) {
c.resolve("", tb_ide::help::command_target(command, designer))
.unwrap();
}
}
}
let t = Temp::new();
let mut a = t.app();
for class in ObjectClass::ALL {
for property in tb_frontend::forms::properties(class) {
a.help_context(property.name, Some(class.name()));
assert!(
a.help.position.query.is_none(),
"{}.{}",
class.name(),
property.name
);
assert_eq!(a.help.page().path, "docs/forms-referenz.md");
let page = a.help.page();
let heading = page
.headings
.iter()
.rev()
.find(|h| h.block <= a.help.position.block)
.unwrap();
assert!(heading.title.to_uppercase().contains(class.name()));
}
}
// The same resolver validates command aliases; a stale anchor is a hard failure.
assert!(c
.resolve(
"",
"docs/ide-bedienung.md#absichtlich-fehlender-befehlsanker"
)
.is_err());
}
#[test]
fn code_and_tables_remain_reachable_after_resize_and_horizontal_scroll() {
let t = Temp::new();
let mut a = t.app();
a.execute(Command::HelpContents);
a.windows
.iter_mut()
.find(|w| w.id == a.active)
.unwrap()
.normal = Rect::new(2, 2, 22, 18);
let (p, _) = Catalog::embedded().resolve("", "docs/tutorial.md").unwrap();
let code = Catalog::embedded().pages[p]
.blocks
.iter()
.position(|b| b.pre && b.text().contains("Hallo aus"))
.unwrap();
a.help.visit(p, code, None);
let before = a.help.position.clone();
let small = draw(&mut a);
assert!(small.contains("PRINT"));
for _ in 0..3 {
key(&mut a, K::Right, M::NONE);
}
assert!(draw(&mut a).contains("Formular"));
let after_scroll = a.help.position.clone();
a.handle(Event::Resize(110, 35));
a.windows
.iter_mut()
.find(|w| w.id == a.active)
.unwrap()
.normal
.width = 70;
draw(&mut a);
assert_eq!(a.help.position, after_scroll);
for _ in 0..3 {
key(&mut a, K::Left, M::NONE);
}
assert_eq!(a.help.position, before);
assert!(draw(&mut a).contains("Hallo aus dem Formular"));
let (p, _) = Catalog::embedded()
.resolve("", "docs/ide-bedienung.md")
.unwrap();
let row = Catalog::embedded().pages[p]
.blocks
.iter()
.position(|b| b.pre && b.text().contains("Index, Contents"))
.unwrap();
a.help.visit(p, row, None);
a.windows
.iter_mut()
.find(|w| w.id == a.active)
.unwrap()
.normal
.width = 22;
let initial = draw(&mut a);
assert!(!initial.contains("Using Help"));
let mut found = false;
for _ in 0..20 {
key(&mut a, K::Right, M::NONE);
found |= draw(&mut a).contains("Using Help");
}
assert!(found);
// Real Unicode paragraphs wrap and remain at the identical logical anchor.
a.execute(Command::UsingHelp);
key(&mut a, K::PageDown, M::NONE);
let pos = a.help.position.clone();
let narrow = draw(&mut a);
assert!(!narrow.contains('\u{fffd}'));
a.windows
.iter_mut()
.find(|w| w.id == a.active)
.unwrap()
.normal
.width = 90;
draw(&mut a);
assert_eq!(a.help.position, pos);
}
#[test]
fn help_preserves_fullscreen_and_does_not_modify_designer_or_source() {
let t = Temp::new();
let mut a = t.app();
a.execute(Command::Output);
a.session.fullscreen = true;
let original = a.active;
key(&mut a, K::F(1), M::SHIFT);
assert!(!a.session.fullscreen);
assert!(draw(&mut a).contains("Hilfe benutzen"));
key(&mut a, K::Esc, M::NONE);
assert_eq!(a.active, original);
assert!(a.session.fullscreen);
a.session.fullscreen = false;
a.execute(Command::NewForm);
let doc = a.designer.document.unwrap();
let before = a.project.document(doc).unwrap().revision();
key(&mut a, K::F(1), M::SHIFT);
for k in [K::Down, K::Right, K::Tab, K::PageDown, K::Delete] {
key(&mut a, k, M::NONE);
}
assert_eq!(a.project.document(doc).unwrap().revision(), before);
for ch in "LEFT$".chars() {
key(&mut a, K::Char(ch), M::NONE);
}
key(&mut a, K::Enter, M::NONE);
assert!(a.help.position.query.as_deref() == Some("LEFT$"));
key(&mut a, K::Tab, M::NONE);
key(&mut a, K::Enter, M::NONE);
assert!(a.help.position.query.is_none());
key(&mut a, K::F(1), M::ALT);
assert_eq!(a.help.position.query.as_deref(), Some("LEFT$"));
assert_eq!(a.project.document(doc).unwrap().revision(), before);
}

View File

@@ -32,7 +32,9 @@ Timer, VScrollBar` — alle außer `Menu` über die Toolbox platzierbar
## Eigenschaften/Methoden/Ereignisse je Steuerelement ## Eigenschaften/Methoden/Ereignisse je Steuerelement
**Form** — Ereignisse: Click, DblClick, DragDrop, DragOver, GotFocus, ### Form
Ereignisse: Click, DblClick, DragDrop, DragOver, GotFocus,
KeyDown, KeyPress, KeyUp, Load, LostFocus, MouseDown, MouseMove, MouseUp, KeyDown, KeyPress, KeyUp, Load, LostFocus, MouseDown, MouseMove, MouseUp,
Paint, Resize, Unload. Methoden: CLS, DRAG, HIDE, LOAD, MOVE, PRINT, Paint, Resize, Unload. Methoden: CLS, DRAG, HIDE, LOAD, MOVE, PRINT,
PRINTFORM, REFRESH, SHOW, TEXTHEIGHT, TEXTWIDTH, UNLOAD. Eigenschaften: PRINTFORM, REFRESH, SHOW, TEXTHEIGHT, TEXTWIDTH, UNLOAD. Eigenschaften:
@@ -41,27 +43,35 @@ DragMode, Enabled, ForeColor, FormName, FormType, Height, Left, MaxButton,
MinButton, MousePointer, Parent, ScaleHeight, ScaleWidth, Tag, Top, MinButton, MousePointer, Parent, ScaleHeight, ScaleWidth, Tag, Top,
Visible, Width, WindowState. Visible, Width, WindowState.
**CommandButton** — Ereignisse: Click, DragDrop, DragOver, GotFocus, ### CommandButton
Ereignisse: Click, DragDrop, DragOver, GotFocus,
KeyDown, KeyPress, KeyUp, LostFocus. Methoden: DRAG, MOVE, REFRESH, KeyDown, KeyPress, KeyUp, LostFocus. Methoden: DRAG, MOVE, REFRESH,
SETFOCUS. Eigenschaften: BackColor, Cancel, Caption, CtlName, Default, SETFOCUS. Eigenschaften: BackColor, Cancel, Caption, CtlName, Default,
DragMode, Enabled, Height, Index, Left, MousePointer, Parent, TabIndex, DragMode, Enabled, Height, Index, Left, MousePointer, Parent, TabIndex,
TabStop, Tag, Top, Value, Visible, Width. Darstellung nach Höhe: TabStop, Tag, Top, Value, Visible, Width. Darstellung nach Höhe:
1 = `<Text>`, 2 = einzeiliger Rahmen, ≥3 = Kasten; Mindestgröße 1×3. 1 = `<Text>`, 2 = einzeiliger Rahmen, ≥3 = Kasten; Mindestgröße 1×3.
**TextBox** — Ereignisse: Change, DragDrop, DragOver, GotFocus, KeyDown, ### TextBox
Ereignisse: Change, DragDrop, DragOver, GotFocus, KeyDown,
KeyPress, KeyUp, LostFocus. Methoden: DRAG, MOVE, REFRESH, SETFOCUS. KeyPress, KeyUp, LostFocus. Methoden: DRAG, MOVE, REFRESH, SETFOCUS.
Eigenschaften: BackColor, BorderStyle, CtlName, DragMode, Enabled, Eigenschaften: BackColor, BorderStyle, CtlName, DragMode, Enabled,
ForeColor, Height, Index, Left, MousePointer, MultiLine, Parent, ForeColor, Height, Index, Left, MousePointer, MultiLine, Parent,
ScrollBars, SelLength, SelStart, SelText, TabIndex, TabStop, Tag, Text, ScrollBars, SelLength, SelStart, SelText, TabIndex, TabStop, Tag, Text,
Top, Visible, Width. Top, Visible, Width.
**ListBox** — Ereignisse: Click, DblClick, DragDrop, DragOver, GotFocus, ### ListBox
Ereignisse: Click, DblClick, DragDrop, DragOver, GotFocus,
Key*, LostFocus, Mouse*. Methoden: ADDITEM, DRAG, MOVE, REFRESH, Key*, LostFocus, Mouse*. Methoden: ADDITEM, DRAG, MOVE, REFRESH,
REMOVEITEM, SETFOCUS. Eigenschaften: BackColor, CtlName, DragMode, Enabled, REMOVEITEM, SETFOCUS. Eigenschaften: BackColor, CtlName, DragMode, Enabled,
ForeColor, Height, Index, Left, List, ListCount, ListIndex, MousePointer, ForeColor, Height, Index, Left, List, ListCount, ListIndex, MousePointer,
Parent, Sorted, TabIndex, TabStop, Tag, Text, Top, Visible, Width. Parent, Sorted, TabIndex, TabStop, Tag, Text, Top, Visible, Width.
**ComboBox** — wie ListBox plus Change, DropDown (Ereignisse), SelLength/ ### ComboBox
wie ListBox plus Change, DropDown (Ereignisse), SelLength/
SelStart/SelText/Style/Text (Eigenschaften). SelStart/SelText/Style/Text (Eigenschaften).
`ListIndex = -1` hebt die Auswahl auch bei leerer Liste auf. ADDITEM vor dem `ListIndex = -1` hebt die Auswahl auch bei leerer Liste auf. ADDITEM vor dem
@@ -77,37 +87,51 @@ Reihenfolge; die Auswahl bleibt beim selben Eintrag, auch bei Duplikaten.
Die Simple-ComboBox zeigt unter dem Eingabefeld ihre Listeneinträge samt Die Simple-ComboBox zeigt unter dem Eingabefeld ihre Listeneinträge samt
Auswahlmarkierung; Inhalt wird auf den verfügbaren Listenbereich begrenzt. Auswahlmarkierung; Inhalt wird auf den verfügbaren Listenbereich begrenzt.
**CheckBox / OptionButton** — Ereignisse: Click (Option auch DblClick), ### CheckBox / OptionButton
Ereignisse: Click (Option auch DblClick),
Drag*, GotFocus, Key*, LostFocus. Methoden: DRAG, MOVE, REFRESH, SETFOCUS. Drag*, GotFocus, Key*, LostFocus. Methoden: DRAG, MOVE, REFRESH, SETFOCUS.
Eigenschaften: BackColor, Caption, CtlName, DragMode, Enabled, ForeColor, Eigenschaften: BackColor, Caption, CtlName, DragMode, Enabled, ForeColor,
Height, Index, Left, MousePointer, Parent, TabIndex, TabStop, Tag, Top, Height, Index, Left, MousePointer, Parent, TabIndex, TabStop, Tag, Top,
Value, Visible, Width. Value, Visible, Width.
**Frame** — nur DragDrop/DragOver; DRAG, MOVE, REFRESH (kein SetFocus); ### Frame
nur DragDrop/DragOver; DRAG, MOVE, REFRESH (kein SetFocus);
Container für Gruppierung (OptionButton-Gruppen). `Index` und `TabIndex` Container für Gruppierung (OptionButton-Gruppen). `Index` und `TabIndex`
werden in den Originaldateien auch für Frames gespeichert. werden in den Originaldateien auch für Frames gespeichert.
**Label** — Ereignisse: Change, Click, DblClick, Drag*, Mouse*. Methoden: ### Label
Ereignisse: Change, Click, DblClick, Drag*, Mouse*. Methoden:
DRAG, MOVE, REFRESH. Eigenschaften: zusätzlich Index, TabIndex, Alignment, DRAG, MOVE, REFRESH. Eigenschaften: zusätzlich Index, TabIndex, Alignment,
AutoSize, BorderStyle. AutoSize, BorderStyle.
**HScrollBar/VScrollBar** — Ereignisse: Change, Drag*, GotFocus, Key*, ### HScrollBar/VScrollBar
Ereignisse: Change, Drag*, GotFocus, Key*,
LostFocus (**kein** separates Scroll-Ereignis). Eigenschaften: zusätzlich LostFocus (**kein** separates Scroll-Ereignis). Eigenschaften: zusätzlich
Attached (VBDOS-Spezifikum: am Formularrand angedockt; Laufzeit read-only), Attached (VBDOS-Spezifikum: am Formularrand angedockt; Laufzeit read-only),
LargeChange, Min, Max, SmallChange, Value. LargeChange, Min, Max, SmallChange, Value.
**Spin** — Ereignisse: Custom(EventType), Drag*, GotFocus, Key*, LostFocus. ### Spin
Ereignisse: Custom(EventType), Drag*, GotFocus, Key*, LostFocus.
`Style` wählt vertikal/horizontal, `Interval` steuert die Wiederholung; `Value` `Style` wählt vertikal/horizontal, `Interval` steuert die Wiederholung; `Value`
läuft zyklisch zwischen `Min` und `Max`. `BorderStyle`, `Width` und `Height` läuft zyklisch zwischen `Min` und `Max`. `BorderStyle`, `Width` und `Height`
sind zur Laufzeit schreibgeschützt. sind zur Laufzeit schreibgeschützt.
**PictureBox** — Text-Zeichenfläche (PRINT/CLS aufs Control) und Container ### PictureBox
Text-Zeichenfläche (PRINT/CLS aufs Control) und Container
für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus, für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus,
Key*, LostFocus, Mouse*, Paint. Methoden: CLS, DRAG, MOVE, PRINT, REFRESH, Key*, LostFocus, Mouse*, Paint. Methoden: CLS, DRAG, MOVE, PRINT, REFRESH,
SETFOCUS, TEXTHEIGHT, TEXTWIDTH. Eigenschaften: zusätzlich Index, TabIndex, SETFOCUS, TEXTHEIGHT, TEXTWIDTH. Eigenschaften: zusätzlich Index, TabIndex,
TabStop, AutoRedraw, CurrentX, CurrentY, ScaleHeight, ScaleWidth. TabStop, AutoRedraw, CurrentX, CurrentY, ScaleHeight, ScaleWidth.
**Timer** — Ereignis Timer, keine Methoden; Eigenschaften: CtlName, Enabled, ### Timer
Ereignis Timer, keine Methoden; Eigenschaften: CtlName, Enabled,
Index, Interval (0 = aus … 65 535 ms), Parent, Tag. Index, Interval (0 = aus … 65 535 ms), Parent, Tag.
Die erste Frist beginnt mit der Aktivierung auf einem sichtbaren Formular Die erste Frist beginnt mit der Aktivierung auf einem sichtbaren Formular
@@ -125,10 +149,14 @@ neu beginnenden aktiven Timern ausgewertet. Die VM erledigt dies nach
Property-Zuweisungen, SHOW und dem Laden von Arrayelementen sowie beim Property-Zuweisungen, SHOW und dem Laden von Arrayelementen sowie beim
Übernehmen eines vorbereiteten Modells; es gibt keinen globalen Zeitcache. Übernehmen eines vorbereiteten Modells; es gibt keinen globalen Zeitcache.
**Menu** — Ereignis Click; Eigenschaften Caption, Checked, CtlName, Enabled, ### Menu
Ereignis Click; Eigenschaften Caption, Checked, CtlName, Enabled,
Index, Parent, Separator, Shortcut, Tag, Visible. Index, Parent, Separator, Shortcut, Tag, Visible.
**DirListBox/DriveListBox/FileListBox** — Datei-Browser-Controls ### DirListBox/DriveListBox/FileListBox
Datei-Browser-Controls
(Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System; (Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System;
Ereignisse u. a. Change, PathChange, PatternChange). Terminal Basic bildet Ereignisse u. a. Change, PathChange, PatternChange). Terminal Basic bildet
sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts). sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts).

41
docs/hilfe.md Normal file
View File

@@ -0,0 +1,41 @@
# Hilfe benutzen
Die Hilfe ist im Programm eingebettet und braucht weder Internet noch ein
Arbeitsverzeichnis mit Dokumentation. Help → Contents zeigt die Referenzen
mit ihren Abschnitten in Lesereihenfolge; Help → Index ordnet sie alphabetisch.
## Kontext und Suche
F1 oder Help → Topic öffnet die Hilfe zum BASIC-Wort unter dem Cursor,
zur gewählten Designerklasse beziehungsweise Eigenschaft oder zum markierten
Menübefehl. Großschreibung und BASIC-Typsuffixe werden berücksichtigt.
Mehrere Treffer erscheinen als auswählbare Links. Ohne Treffer bleibt das
Suchwort im Index sichtbar. Im Hilfefenster kann ein Suchwort direkt eingegeben
und mit Enter gesucht werden; Backspace korrigiert es.
Options → Right Mouse aktiviert dieselbe Kontext-Hilfe per Rechtsklick.
## Navigation
Interne Links erscheinen als ◄Thema►. Tab und Shift+Tab wählen den nächsten
beziehungsweise vorherigen Link; Enter folgt dem markierten Link. Alt+F1
kehrt zu einem der letzten 20 Besuchszustände zurück, einschließlich
Leseposition und Linkfokus. Ältere Zustände werden verdrängt. Ctrl+F1 öffnet
die nächste Datei in der Contents-Reihenfolge. Am Ende erscheint ein Hinweis.
Pfeil auf/ab und Page Up/Down scrollen, Home/End springen zum Anfang/Ende.
Pfeil links/rechts verschiebt breite Tabellen und Code horizontal.
Fließtext wird an die Breite angepasst; eine Größenänderung erhält den
logischen Leseanker und den markierten Link. Bei sehr schmaler Ansicht
können breite Unicode-Zeichen horizontal gescrollt werden.
Esc schließt die Hilfe und stellt den vorherigen Fenster- und Property-Fokus
wieder her. Die allgemeinen Fensterbefehle bleiben verfügbar.
## Linkziele
Externe Links tragen zusätzlich „extern“ und zeigen ihre URL. Enter zeigt
nur einen Hinweis: Die Hilfe startet keinen Browser, Prozess oder Download.
Fehlende Dateien oder Anker erzeugen eine Diagnose. Es werden ausschließlich
mitgelieferte Dokumente angezeigt, keine beliebigen lokalen Dateien.
Weiter: [Tastatur](tastatur.md), [Tutorial](tutorial.md),
[IDE-Bedienung](ide-bedienung.md).

View File

@@ -127,11 +127,11 @@ Library-Symbole/Aufrufvertrag und der native Verbraucher-Nachweis stehen
explizit in [PLAN.md, Phase 6](../PLAN.md). Ein umbenanntes TBC erfüllt explizit in [PLAN.md, Phase 6](../PLAN.md). Ein umbenanntes TBC erfüllt
keines der beiden Exportziele. keines der beiden Exportziele.
## Befehlsabdeckung nach Change 06 ## Befehlsabdeckung nach Change 07
Alle Referenzbefehle sind in ihrer Menüfolge vorhanden. Fachbefehle der Alle Referenzbefehle sind in ihrer Menüfolge vorhanden und angebunden.
folgenden Changes sind sichtbar deaktiviert und zeigen einen Nicht verfügbare Aktionen zeigen einen zustandsabhängigen Grund; ihre
Verfügbarkeitsgrund; ihre Kürzel gelangen nicht versehentlich an BASIC. Kürzel gelangen nicht versehentlich an BASIC.
Ctrl+C im Editor ist Kopieren, bei laufendem Programm mit Ctrl+C im Editor ist Kopieren, bei laufendem Programm mit
Output-Fokus unterbricht Ctrl+C sofort die Sitzung, ohne eine BASIC-Taste einzureihen. Output-Fokus unterbricht Ctrl+C sofort die Sitzung, ohne eine BASIC-Taste einzureihen.
@@ -143,12 +143,12 @@ Output-Fokus unterbricht Ctrl+C sofort die Sitzung, ohne eine BASIC-Taste einzur
| Search | Find, Selected Text, Repeat Last Find, Change | — | | Search | Find, Selected Text, Repeat Last Find, Change | — |
| Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Native Erzeugung: Phase 6 | | Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Native Erzeugung: Phase 6 |
| Debug | Breakpoints, Schritte, Calls, Watches/Watchpoints, Direktfenster, Trace/History, Break on Errors und Set Next Statement | — | | Debug | Breakpoints, Schritte, Calls, Watches/Watchpoints, Direktfenster, Trace/History, Break on Errors und Set Next Statement | — |
| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking einschließlich Zeilenprüfung | Kontext-Hilfe: 07 | | Options | Display, Set Paths, Right Mouse, Save, Syntax Checking einschließlich Zeilenprüfung | |
| Window | New Window, Arrange All, Calls/Debug/Help/Immediate/Output/Project, aktive Codeliste, alle Fensterkürzel | Hilfeinhalt: 07 | | Window | New Window, Arrange All, Calls/Debug/Help/Immediate/Output/Project, aktive Codeliste, alle Fensterkürzel | |
| Designer Tools/Window | Vollständige Toolbox einschließlich Spin, Formliste, Color Palette, Menu Design Window | — | | Designer Tools/Window | Vollständige Toolbox einschließlich Spin, Formliste, Color Palette, Menu Design Window | — |
| Help | Vollständiges Menü, About und Help-Fensterrahmen | Index, Contents, Keyboard, Topic, Using Help, Tutorial: 07 | | Help | Index, Contents, Keyboard, Topic, Using Help, Tutorial, About | — |
Editor, inkrementelle Übersetzung, Ausführung, Formulardesigner und Debugger sind umgesetzt. Die gesamte Editor, inkrementelle Übersetzung, Ausführung, Formulardesigner, Debugger und Hilfe sind umgesetzt. Die gesamte
Phase-5-Abnahme erfolgt in Change 08. Phase-5-Abnahme erfolgt in Change 08.
## Programme ausführen ## Programme ausführen
@@ -469,3 +469,19 @@ Cursorziele setzen den unveränderten Quellstand der pausierten Sitzung voraus;
nach Änderungen zuerst neu starten. F5 kann weiterhin nach ausdrücklicher nach Änderungen zuerst neu starten. F5 kann weiterhin nach ausdrücklicher
Auswahl das gekennzeichnete alte Kompilat fortsetzen; seine Watches verwenden Auswahl das gekennzeichnete alte Kompilat fortsetzen; seine Watches verwenden
die ursprünglichen Symbole und Slots. die ursprünglichen Symbole und Slots.
## Offline-Hilfe
Help → Contents zeigt die eingebetteten Dokumente mit ihren Abschnitten,
Help → Index dieselben Ziele alphabetisch. F1 öffnet den Kontext zum
BASIC-Wort, zur Designerklasse/-eigenschaft oder zum markierten Menübefehl.
Shift+F1 erklärt die [Hilfebedienung](hilfe.md); [Tastatur](tastatur.md) und
[Tutorial](tutorial.md) beschreiben die implementierten Arbeitsabläufe.
Tab/Shift+Tab wählen Links, Enter folgt, Alt+F1 stellt bis zu 20 vorherige
Lesepositionen wieder her. Ctrl+F1 folgt der Contents-Dateireihenfolge.
Pfeile und Page Up/Down scrollen; links/rechts verschiebt breite Code- und
Tabellenzeilen. Größenänderungen behalten Thema, Leseanker und Linkfokus.
Esc kehrt zum vorherigen Fenster zurück. Die Hilfe braucht keinen Checkout
und öffnet externe Links ausschließlich als sichtbaren Hinweis.

46
docs/tastatur.md Normal file
View File

@@ -0,0 +1,46 @@
# Tastatur der IDE
## Allgemeine Bedienung
| Taste | Aktion |
| --- | --- |
| Alt+Buchstabe, F11 | Menü öffnen; Pfeile wählen, Enter bestätigt, Esc schließt |
| F6 / Shift+F6 | Nächstes / vorheriges Fenster |
| Ctrl+F4 | Aktives Fenster schließen |
| Ctrl+F7 / Ctrl+F8 | Fenster verschieben / Größe ändern; Pfeile, Enter, Esc |
| Ctrl+F9 / Ctrl+F10 / Ctrl+F5 | Minimieren / maximieren / wiederherstellen |
| F1 / Shift+F1 | Kontext-Hilfe / Hilfe benutzen |
| F2 / Shift+F12 | Code / Formular |
| F12 | Ereignisprozeduren |
| Ctrl+Z / Ctrl+X / Ctrl+C / Ctrl+V | Undo / Ausschneiden / Kopieren / Einfügen |
| F3 | Suche wiederholen |
| Shift+F2 / Ctrl+F2 | Prozeduren / vorheriger Codeort |
## Ausführung und Debugger
| Taste | Aktion |
| --- | --- |
| Shift+F5 / F5 | Start / Continue |
| Ctrl+Break | Unterbrechen |
| Ctrl+C im laufenden Output | Unterbrechen |
| F4 | Output-Vollbild umschalten |
| F7 / F8 / F10 | Run to Cursor / Einzelschritt / Prozedurschritt |
| F9 / Shift+F9 | Breakpoint / Instant Watch |
| Shift+F8 / Shift+F10 | History zurück / vorwärts |
## Designer
Pfeile verschieben die Auswahl, Ctrl+Pfeile um fünf Zellen; Shift+Pfeile
ändern die Größe. Tab/Shift+Tab wechseln das Control. F2 aktiviert das
Property-Wertfeld. Page Up/Down wechseln Eigenschaften. F10 wechselt
zwischen Property-Leiste und Menü. F12 führt zum Ereigniscode.
## Hilfefenster
Tab/Shift+Tab wählen Links, Enter folgt, Alt+F1 springt zurück und Ctrl+F1
zum nächsten Thema. Pfeile und Page Up/Down scrollen, Home/End wählen
Anfang/Ende. Esc kehrt zum vorherigen Fenster zurück.
Details: [Hilfe benutzen](hilfe.md), [Editor](ide-bedienung.md#editor-suche-und-quelltextpflege),
[Designer](ide-bedienung.md#formulare-gestalten),
[Debugger](ide-bedienung.md#debugger-und-direktfenster).

56
docs/tutorial.md Normal file
View File

@@ -0,0 +1,56 @@
# Tutorial: Vom Formular zum laufenden Programm
## Projekt und Formular erstellen
File → New Project erzeugt ein Projekt. File → New Form öffnet ein neues
Formular im Designer. Bereits vorhandene ungespeicherte Arbeit lässt sich
im Wechsel-Dialog speichern, verwerfen oder mit Cancel behalten.
File → Save Project speichert das Projekt und seine Mitglieder; die Pfade
und mögliche Überschreibbestätigungen stehen im Speicherdialog.
## Oberfläche gestalten
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
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).
## Ereigniscode schreiben
F12 öffnet die Ereignisauswahl des gewählten Controls. Wähle Click und
bestätige. Die IDE öffnet beziehungsweise erzeugt die passende Prozedur.
Schreibe zwischen SUB und END SUB:
```basic
PRINT "Hallo aus dem Formular"
```
Mit Shift+F12 gelangst du wieder zum Formular. Syntaxfehler erscheinen
beim Verlassen der Zeile beziehungsweise unter View → Diagnostics.
Siehe [Editor und Übersetzung](ide-bedienung.md#editor-suche-und-quelltextpflege).
## Debuggen und ausführen
Setze im Ereigniscode mit F9 einen Breakpoint auf PRINT. Run → Set Start-up
File wählt das Formular als Startdatei. Shift+F5 startet das Projekt.
Betätige den Button im laufenden Formular: Die IDE hält vor PRINT und
zeigt den Quellort. F8 führt die Anweisung aus. F5 setzt das Programm fort.
F4 schaltet die Programmausgabe ins Vollbild und zurück; Ctrl+Break
unterbricht die Sitzung. Run → Restart beginnt erneut.
Window → Calls zeigt Aufrufrahmen; Debug → Add Watch beobachtet Ausdrücke.
Window → Immediate öffnet das Direktfenster für Kommandos im pausierten
Kontext. [Debugger und Direktfenster](ide-bedienung.md#debugger-und-direktfenster)
erklärt Fehlerhalte, Watchpoints und die Grenzen von Set Next Statement.
## Speichern und nächste Schritte
File → Save Project hält den Stand fest. Make EXE File und Make Library
bieten bereits die Zielauswahl; die tatsächliche Erzeugung eines nativen
Standalone-Programms beziehungsweise einer nativen Library folgt in Phase 6.
Siehe [Exportgrenze](ide-bedienung.md#native-exporte-ui-in-phase-5-erzeugung-in-phase-6).
Weiter: [Tastatur](tastatur.md), [Hilfe benutzen](hilfe.md),
[Sprachreferenz](sprachreferenz.md), [Bibliothek](bibliothek.md).

View File

@@ -34,7 +34,7 @@ Besondere Risiken sind unverändert ausgeführte alte Kompilate nach einer Text
| 04 | [Ausführung und Output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md) | 01, 02, 03 | Fortsetzbare VM-Sitzung, Reset, Ausgabe, Shell und Textdruck | | 04 | [Ausführung und Output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md) | 01, 02, 03 | Fortsetzbare VM-Sitzung, Reset, Ausgabe, Shell und Textdruck |
| 05 | [Formular-Designer](../2026-09-06-phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode | | 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 | | 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](../../phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation | | 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) | 0107 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status | | 08 | [Integration und Phasenabnahme](../../phase-5-08-integration-und-phasenabnahme/proposal.md) | 0107 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status |
```text ```text

View File

@@ -23,6 +23,6 @@ Keine bestehenden Anforderungen werden ersetzt; die neue IDE-Fähigkeit ergänzt
tb-ide-Hilfe, docs/ und Paketierung von tb; nutzt Fensterverwaltung und Editor-Kontext. Kein Webdienst und keine eigene zweite Dokumentationssammlung. tb-ide-Hilfe, docs/ und Paketierung von tb; nutzt Fensterverwaltung und Editor-Kontext. Kein Webdienst und keine eigene zweite Dokumentationssammlung.
**Abhängigkeiten:** [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). **Abhängigkeiten:** [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).
**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.

View File

@@ -1,17 +1,17 @@
## 1. Dokumente und Darstellung ## 1. Dokumente und Darstellung
- [ ] 1.1 Die relevanten docs-Inhalte mit stabilem Datei-/Themen-/Ankerkatalog in tb einbetten; ein Test startet die Hilfe aus einem temporären Verzeichnis ohne Checkout und findet alle Referenzseiten. - [x] 1.1 Die relevanten docs-Inhalte mit stabilem Datei-/Themen-/Ankerkatalog in tb einbetten; ein Test startet die Hilfe aus einem temporären Verzeichnis ohne Checkout und findet alle Referenzseiten.
- [ ] 1.2 Den Markdown-Block-/Inline-Pfad mit vorhandenen Abhängigkeiten oder einem einzelnen geeigneten Parser anbinden; Renderproben prüfen Überschriften, Listen, Escapes, Links, Codeblöcke und Tabellen. - [x] 1.2 Den Markdown-Block-/Inline-Pfad mit vorhandenen Abhängigkeiten oder einem einzelnen geeigneten Parser anbinden; Renderproben prüfen Überschriften, Listen, Escapes, Links, Codeblöcke und Tabellen.
- [ ] 1.3 Dynamischen Umbruch und horizontales Scrollen für breite Inhalte implementieren; TestBackend-Prüfungen mit Unicode und Resize erhalten Thema, Leseposition und Linkfokus. - [x] 1.3 Dynamischen Umbruch und horizontales Scrollen für breite Inhalte implementieren; TestBackend-Prüfungen mit Unicode und Resize erhalten Thema, Leseposition und Linkfokus.
## 2. Kontext und Navigation ## 2. Kontext und Navigation
- [ ] 2.1 F1-/Rechtsklick-Kontext aus Editor, Forms-Property und IDE-Befehl auf Themen/Anker abbilden; Tests prüfen Typ-Suffixe, Großschreibung, Mehrdeutigkeit und fehlenden Treffer. - [x] 2.1 F1-/Rechtsklick-Kontext aus Editor, Forms-Property und IDE-Befehl auf Themen/Anker abbilden; Tests prüfen Typ-Suffixe, Großschreibung, Mehrdeutigkeit und fehlenden Treffer.
- [ ] 2.2 Tab/Shift+Tab, Enter, Alt+F1, Ctrl+F1 und Esc integrieren; Eingabetests belegen Linkfolge, 20 Rücksprünge, Ende der Themenreihenfolge und vorherigen Fensterfokus. - [x] 2.2 Tab/Shift+Tab, Enter, Alt+F1, Ctrl+F1 und Esc integrieren; Eingabetests belegen Linkfolge, 20 Rücksprünge, Ende der Themenreihenfolge und vorherigen Fensterfokus.
- [ ] 2.3 Interne Pfade/Anker prüfen und externe Ziele sichtbar ohne automatische Ausführung behandeln; Tests belegen fehlende Ziele, Pfadbegrenzung und unterbleibende Prozess-/Netzwerkaufrufe. - [x] 2.3 Interne Pfade/Anker prüfen und externe Ziele sichtbar ohne automatische Ausführung behandeln; Tests belegen fehlende Ziele, Pfadbegrenzung und unterbleibende Prozess-/Netzwerkaufrufe.
## 3. Hilfe-Einstiege und Abnahme ## 3. Hilfe-Einstiege und Abnahme
- [ ] 3.1 Index, Contents, Keyboard, Using Help, Tutorial und About anbinden und fehlende Bedienungsseiten unter docs ergänzen; Menü-/Shortcut-Tests öffnen jeden Einstieg und zeigen die gebaute Version. - [x] 3.1 Index, Contents, Keyboard, Using Help, Tutorial und About anbinden und fehlende Bedienungsseiten unter docs ergänzen; Menü-/Shortcut-Tests öffnen jeden Einstieg und zeigen die gebaute Version.
- [ ] 3.2 Alle eingebetteten relativen Links und Kontextaliasse mit einer ausführbaren Katalogprüfung absichern; sie muss bei absichtlich fehlendem Ziel scheitern. - [x] 3.2 Alle eingebetteten relativen Links und Kontextaliasse mit einer ausführbaren Katalogprüfung absichern; sie muss bei absichtlich fehlendem Ziel scheitern.
- [ ] 3.3 Die Hilfe-Spec-Szenarien in verification.md zuordnen und cargo test -p tb-ide sowie Formatprüfung ausführen; der Tutorial-/Designer-Gesamtweg wird zusätzlich in 08 geprüft. - [x] 3.3 Die Hilfe-Spec-Szenarien in verification.md zuordnen und cargo test -p tb-ide sowie Formatprüfung ausführen; der Tutorial-/Designer-Gesamtweg wird zusätzlich in 08 geprüft.

View File

@@ -0,0 +1,84 @@
# Verification Report: phase-5-07-hilfesystem
Stand: 2026-09-07. Proposal, Design, neun Aufgaben sowie vier Anforderungen
mit sechs Szenarien wurden gegen den aktuellen Code und ausführbare Tests geprüft.
## Ergebnis
| Dimension | Ergebnis |
| --- | --- |
| Completeness | 9/9 Aufgaben; 4/4 Anforderungen umgesetzt |
| Correctness | 6/6 Szenarien zugeordnet und geprüft |
| Coherence | Eingebettete docs als einzige Textquelle; vorhandene App-, Fenster-, Designer- und Renderingpfade |
| Offene Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION |
## Anforderungen und Szenarien
Implementierung: `crates/tb-ide/src/help.rs`, Dispatcher in `app.rs`,
Renderaufruf in `render.rs`, Property-Kontext in `designer.rs`.
Neue Nachweise: `crates/tb-ide/tests/help.rs`.
| Requirement / Scenario | Implementierung und Nachweis |
| --- | --- |
| Mitgelieferte Dokumente / Start außerhalb des Repositorys | `DOCUMENTS` bindet elf aktuelle docs-Dateien und den verlinkten PLAN direkt per `include_str!` ein. `Catalog` erzeugt Index und Contents aus diesen Seiten. `help_works_from_a_directory_without_checkout` startet den Testprozess in einem temporären Verzeichnis ohne docs; `offline_help_child` öffnet dort dieselbe `App`, alle Hilfe-Einstiege sowie Sprach-, Bibliotheks-, Forms-, IDE-Referenz und Tutorial mit TestBackend. Der normale tb-Einstieg benutzt diese App unabhängig vom Arbeitsverzeichnis. |
| Markdown / Schmales Hilfefenster | `Page::parse` verarbeitet CommonMark einschließlich Tabellen mit pulldown-cmark; `layout` berechnet Fließtextzeilen anhand der Zellbreite. Code und Tabellen behalten ihre vollständigen Zeichen und sind horizontal scrollbar. `Position` speichert Seite, Block, Zeichenoffset und Linkfokus. `markdown_blocks_escapes_links_and_unicode_layout` prüft Listen, Formatierung, Escapes, Links, Unicode und verlustfreie Blockprojektion bei vier Breiten. `code_and_tables_remain_reachable_after_resize_and_horizontal_scroll` und `links_history_resize_scroll_and_focus_use_real_events` prüfen schmale TestBackend-Fenster, echte Scrolltasten und Resize bei erhaltenem Leseanker und Linkfokus. |
| Kontextsensitive Themenwahl / Property und BASIC-Funktion | `token_at`, `normalize`, `Catalog::search`, `help_context` und `design_help_context` verbinden BASIC-Tokens samt Typsuffix mit Dokumentabschnitten und Designerklasse/-eigenschaft. `command_target` bildet konkrete Befehls-IDs ab. `context_functions_properties_commands_and_unknown_queries` prüft F1 auf `lEfT$`, Form.Caption im aktiven Property-Wertfeld, den fokussierten Save-Project-Menübefehl, Mehrdeutigkeit und unbekanntes Suchwort. `every_command_and_forms_alias_resolves_and_invalid_alias_fails` prüft alle Menübefehle beider Modi und alle Properties der 19 Forms-Klassen. |
| Links und Verlauf / Rücksprung mit Leseposition | `Help::visit`, `follow`, `back`, `focus_link`, `help_key` halten Navigation und Fenstersitzung zusammen. `links_history_resize_scroll_and_focus_use_real_events` folgt nach Scrollen per Enter einem Link und stellt mit Alt+F1 exakt Seite, Offset und Linkfokus wieder her. Esc restauriert das vorherige Fenster; `help_preserves_fullscreen_and_does_not_modify_designer_or_source` prüft zusätzlich Output-Vollbild, Designerzustand und Suchergebnis-Rücksprünge. |
| Links und Verlauf / Grenze des Verlaufs | `Help::visit` hält maximal 20 Rücksprungzustände. `twenty_back_steps_end_of_contents_and_all_entries` führt 25 Wechsel und anschließend die letzten 20 Rücksprünge aus. Weiteres Zurück verändert keinen Zustand und zeigt den Grenzhinweis. Ctrl+F1 folgt der Contents-Dateireihenfolge und bleibt am Ende mit Hinweis stehen. |
| Hilfe-Einstiege / Tutorial folgen | Alle sieben Help-Einstiege sind angebunden; About zeigt die gebaute Paketversion und Autoren. `twenty_back_steps_end_of_contents_and_all_entries` bedient Menüs und Shortcuts. `docs/tutorial.md` wurde gegen New Project/Form, Toolbox-Platzierung, Caption/F2, F12-Ereignisauswahl, Startdatei, F9/F8/F5 und die vorhandenen Designer-/Debugger-/Ausführungstests abgeglichen. Alle Tutorialverweise werden durch `Catalog::validate` geprüft. Der zusätzliche durchgehende Tutorial-/Designer-Abnahmelauf bleibt gemäß Aufgabe 3.3 in Change 08. |
## Weitere Vertragsprüfungen
- `catalog_links_and_missing_targets_are_checked` prüft sämtliche internen
Kataloglinks und Überschriftenanker. Absichtlich fehlende Dateien, Anker,
ungültige Prozentkodierungen und Pfade außerhalb des Katalogs schlagen fehl.
Doppelte Überschriften einschließlich Namenskollisionen erhalten eindeutige Anker.
- Kontextaliasse werden durch dieselbe Zielauflösung geprüft; ein absichtlich
falscher Befehlsanker erzeugt einen Fehler. Klasse und Property stammen aus
der vorhandenen Forms-Metadatenquelle, die Texte weiterhin aus docs.
- `right_click_configuration_and_external_link_follow` prüft den Optionsschalter,
sichtbare externe URLs und Enter auf einem externen Ziel. Es erfolgt kein
Shell-Auftrag; die Hilfe besitzt keinen Datei-, Prozess- oder Netzwerkpfad.
Die bestehende App-Regression belegt weiterhin Rechtsklick-Zustellung an ein
laufendes BASIC-Programm mit Output-Fokus.
- Index sortiert Themen und Abschnitte alphabetisch, Contents bildet die
Dokument-/Überschriftenhierarchie ab. Suche ohne Treffer erhält das Suchwort
und bietet Index/Contents an; Mehrdeutigkeit erzeugt eine bedienbare Linkliste.
- `docs/hilfe.md`, `docs/tastatur.md` und `docs/tutorial.md` sind normale
eingebettete Markdown-Seiten. Es gibt weder eine zweite Referenzsammlung
noch externe Renderer. Die Build-Abhängigkeit pulldown-cmark 0.13.4 wurde
nach Prüfung der CommonMark-/Tabellen-API und MIT-Lizenz ohne Default-Features
aufgenommen. Darstellungsbreiten und Fenster verwenden vorhandene Bibliotheken.
## Behobene Befunde
- Frühes Rechtsklick-Routing fing anfangs Programmmauseingaben ab. Kontext-Hilfe
respektiert jetzt den laufenden Output-Fokus; die bisherige App-Regression besteht.
- Hilfe aus Output-Vollbild wurde vom Vollbild verdeckt. Öffnen und Schließen
sichern und restaurieren nun auch diesen Fokuszustand.
- Allgemeine Menügruppen waren für einzelne Befehle zu ungenau. Eine exhaustive
Zuordnung der konkreten Befehls-IDs verweist auf die jeweiligen Bedienabschnitte;
der Katalogtest sichert die Ziele ab.
- Einfache Überschriftenzähler konnten bei einem bereits vorhandenen Suffix
kollidieren. Eine Menge der vergebenen Anker verhindert solche Kollisionen.
- Benachbarte Inline-Spans gleichen Stils werden zusammengefasst, damit
kombinierende Unicode-Zeichen bei der Terminaldarstellung zusammenbleiben.
- Die bisherige Phase-07-Platzhalterdarstellung und die vollständig überholte
Feature-Phasen-Sperre wurden entfernt. Hilfe und Bedienungsübersicht zeigen
den implementierten Stand; die native Exporterzeugung bleibt Phase 6.
## Abschlussprüfungen
- `cargo test -p tb-ide`: 79 Tests bestanden, keine Fehler oder Ignore-Fälle;
zusätzlich ein erfolgreicher Offline-Kindprozess mit derselben Hilfe-App.
Enthalten sind elf Hilfetests sowie alle bestehenden App-, Dokument-, Editor-,
Designer-, Debugger- und Ausführungstests.
- `cargo clippy --workspace --all-targets -- -D warnings`: bestanden.
- `cargo fmt --all -- --check`: bestanden.
- `git diff --check`: bestanden.
- `openspec validate --all --strict`: 23/23 gültig. Bestehende INFO-Hinweise
zu langen Anforderungstexten sind keine Validierungsfehler.
Keine Prüfdimension des Changes wurde ausgelassen. Der zusätzliche Gesamtweg
in Change 08 gehört wie geplant zur späteren Phasenabnahme. Es bestehen keine
offenen Befunde; der Change ist zur Synchronisierung und Archivierung bereit.

View File

@@ -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. 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](../phase-5-07-hilfesystem/proposal.md). **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).
**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](../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.

View File

@@ -0,0 +1,43 @@
# ide-hilfe Specification
## Purpose
Macht die mitgelieferte Markdown-Dokumentation als offline verfügbare, kontextsensitive IDE-Hilfe mit verlässlicher Navigation und variabler Fenstergröße nutzbar.
## Requirements
### Requirement: Mitgelieferte Dokumente und Markdown-Darstellung
Die Hilfe SHALL die aktuelle Dokumentation aus docs einschließlich Sprach-, Bibliotheks-, Forms- und IDE-Bedienungsreferenz ohne Netzwerk und unabhängig vom Arbeitsverzeichnis bereitstellen. Überschriften, Absätze, Listen, Codeblöcke, Tabellen und Links SHALL lesbar dargestellt werden; Fließtext SHALL nach Fensterbreite umbrechen, Code und Tabellen SHALL ohne Informationsverlust umgebrochen oder horizontal scrollbar sein. Resize SHALL Thema und logische Leseposition erhalten.
#### Scenario: Start außerhalb des Repositorys
- **WHEN** das gebaute tb aus einem Verzeichnis ohne docs gestartet wird
- **THEN** sind Index, Referenzthemen und Tutorial weiterhin vollständig erreichbar
#### Scenario: Schmales Hilfefenster
- **WHEN** ein Thema mit Tabelle, Unicode und Code bei schmaler Fensterbreite geöffnet und anschließend vergrößert wird
- **THEN** bleiben alle Inhalte erreichbar, Links bedienbar und die Leseposition demselben Inhalt zugeordnet
### Requirement: Kontextsensitive Themenwahl
F1/Topic und entsprechend konfigurierter Rechtsklick SHALL das Thema zum Token unter dem Editorcursor, zur Designerklasse/-eigenschaft oder zum fokussierten IDE-Befehl öffnen. Sprache und Bibliothek SHALL unabhängig von Großschreibung und mit Typ-Suffixen gefunden werden. Bei mehreren passenden Themen SHALL eine Auswahl erscheinen; ohne passenden Kontext SHALL eine verständliche Suche beziehungsweise der Index erscheinen.
#### Scenario: Property und BASIC-Funktion
- **WHEN** F1 zuerst auf einer Formulareigenschaft und danach auf einer BASIC-Funktion mit Dollar-Suffix ausgelöst wird
- **THEN** öffnet die Hilfe jeweils das zugehörige dokumentierte Thema einschließlich dessen passendem Abschnitt
### Requirement: Links und Verlauf
Interne Links SHALL als ◄Thema► sichtbar sein. Tab/Shift+Tab SHALL den Linkfokus bewegen, Enter SHALL folgen, Alt+F1 SHALL bis zu 20 besuchte Zustände zurückführen und Ctrl+F1 SHALL das nächste Thema in der dokumentierten Themenreihenfolge öffnen. Esc SHALL die Hilfe schließen und den vorherigen Fokus wiederherstellen. Fehlende Ziele SHALL eine Diagnose liefern; externe Links SHALL als extern erkennbar sein und nicht automatisch einen Prozess oder Netzwerkzugriff auslösen.
#### Scenario: Rücksprung mit Leseposition
- **WHEN** ein Link nach Scrollen verfolgt und Alt+F1 gedrückt wird
- **THEN** kehrt die Hilfe zu Thema, Leseposition und Linkfokus vor dem Sprung zurück
#### Scenario: Grenze des Verlaufs
- **WHEN** mehr als 20 Themenwechsel erfolgen
- **THEN** bleiben die letzten 20 Rücksprünge möglich, ältere Einträge werden nachvollziehbar verdrängt und weiterer Rücksprung verändert den Zustand nicht
### Requirement: Hilfe-Einstiege
Help SHALL Index, Contents, Keyboard, Topic/F1, Using Help/Shift+F1, Tutorial und About anbieten. Index SHALL Themen alphabetisch auffindbar machen und Contents SHALL die Dokumenthierarchie abbilden. Keyboard und Tutorial SHALL die tatsächlich implementierte IDE-Bedienung erklären; About SHALL Version und Copyright anzeigen.
#### Scenario: Tutorial folgen
- **WHEN** der Benutzer Tutorial öffnet und den dort beschriebenen Weg zum Erstellen, Gestalten, Debuggen und Ausführen verfolgt
- **THEN** stimmen Befehle und Tasten mit der Oberfläche überein und alle internen Verweise sind auflösbar