← Commit history

Merge PR #416: 1.0.101: kicad_show_library shows the library instead of walking its parts

Colby Knox ·517bf73afa ·6d ago ·parent ffc42b6
4 files changed +90−19
rust/Cargo.lock+3−3
@@ -319,7 +319,7 @@ dependencies = [  [[package]] name = "kicad-bridge"-version = "1.0.100"+version = "1.0.101" dependencies = [  "adom-eda-checks",  "kicad-core",@@ -331,7 +331,7 @@ dependencies = [  [[package]] name = "kicad-core"-version = "1.0.100"+version = "1.0.101" dependencies = [  "kicad-ipc-rs",  "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [  [[package]] name = "kicad-platform"-version = "1.0.100"+version = "1.0.101" dependencies = [  "serde",  "serde_json",
rust/Cargo.toml+1−1
@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"]  [workspace.package]-version = "1.0.100"+version = "1.0.101" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_show.rs+1−1
@@ -123,7 +123,7 @@ pub static VERBS: &[Verb] = &[         mechanism: Mechanism::Window, risk: "read", timeout_sec: 130,         input: "{\"libraryName\": \"Device\", \"kind\"?: \"symbol\"|\"footprint\", \"symbolName\"?: \"...\", \"footprintName\"?: \"...\", \"capture\"?: bool, \"foreground\"?: bool, \"foregroundReason\"?: \"...\"}",         example: "kicad_show_library {\"libraryName\":\"Device\"}",-        hint: "kicad_show_library {\"libraryName\":\"Device\"} (add kind:\"footprint\" for the fp side). Filters the tree to the library. A symbolName/footprintName passed alongside is honoured, not dropped (#49 follow-up).",+        hint: "kicad_show_library {\"libraryName\":\"Device\"} (add kind:\"footprint\" for the fp side). Lists the whole library in the tree and loads nothing, in the background. A warm editor's canvas keeps the part it had: canvasPart names it and canvasFromOtherLibrary says if a proof image shows another library's part. A symbolName/footprintName passed alongside is honoured, not dropped (#49 follow-up).",         related: &["kicad_show_symbol", "kicad_show_footprint", "kicad_list_symbols"],         pitfalls: &["the library must be registered in the sym-/fp-lib-table"],     },
rust/crates/kicad-bridge/src/verbs_windows.rs+85−14
@@ -1435,6 +1435,39 @@ fn verify_title(title: &str, v: Verify<'_>) -> bool { /// LIB_TREE filter, Down through the filter box, Enter posted to the tree candidates, /// Down x1..3 then Enter, a posted double-click on the first row. Zero focus. /// Returns (confirmed, trace, mechanism).+/// The tree filter that lists one library whole. KiCad reads "Lib:" as "every part of Lib" and+/// nothing else; the bare name is a text search, which drops parts that don't say it and pulls in+/// other libraries' parts that do. KiCad splits the filter on spaces, so a name with one stays bare.+fn library_filter(library: &str) -> String {+    let name = library.trim().trim_end_matches(':');+    if name.chars().any(char::is_whitespace) { name.to_string() } else { format!("{name}:") }+}++/// Whether a navigation goes on to load a part, or only filters the tree.+fn loads_a_part(verify: Verify<'_>) -> bool {+    !matches!(verify, Verify::Never)+}++/// The reply of a library browse. The canvas is left alone, so on a warm editor it still shows+/// whatever was there: say so, because a proof image of it shows that part and not the library's.+fn library_browse_reply(ok: bool, title: &str, editor_word: &str, library: &str, nothing: &str) -> Value {+    if !ok {+        return json!({"success": false, "errorCode": "library_filter_failed", "error": format!("The {editor_word} is open but its library filter could not be set to '{library}'"),+            "_hint": "Read navigation.steps. A hidden library tree pane is the usual cause: show it from the editor's View menu."});+    }+    let (lib, part) = wm::parse_editor_title(title, editor_word);+    let mut out = json!({"success": true, "libraryShown": library});+    if part.is_empty() {+        out["output"] = json!(format!("{editor_word} open with the '{library}' library filtered. {nothing}"));+    } else {+        let id = if lib.is_empty() { part.clone() } else { format!("{lib}:{part}") };+        out["canvasPart"] = json!(id);+        out["canvasFromOtherLibrary"] = json!(!lib.eq_ignore_ascii_case(library));+        out["output"] = json!(format!("{editor_word} open with the '{library}' library filtered. This call loaded nothing; the canvas still shows {id} from before."));+    }+    out+}+ fn navigate_tree(frame: u64, filter: &str, verify: Verify<'_>) -> (bool, Value, &'static str) {     let mut steps: Vec<String> = Vec::new();     let mut trace = json!({"filter": filter, "editorHwnd": frame, "titleBefore": title_of(frame)});@@ -1472,6 +1505,13 @@ fn navigate_tree(frame: u64, filter: &str, verify: Verify<'_>) -> (bool, Value,         }     }     sleep_ms(1400); // LIB_TREE search debounce on the UI thread + tree rebuild+    // A library browse asks for no part: everything below this exists to load one, and with no+    // part to confirm it would walk the library row by row and leave the last one on the canvas.+    if !loads_a_part(verify) {+        steps.push("library browse: filter set, no part loaded".into());+        trace["steps"] = json!(steps);+        return (true, trace, mechanism);+    }     // Down THROUGH the filter box: KiCad forwards nav keys to the tree.     post_vk(edit.hwnd, wm::VK_DOWN);     sleep_ms(500);@@ -2120,13 +2160,13 @@ fn open_symbol_editor_inner(info: &KicadInfo, args: &Value, symbol_name: &str, l     }     // Library-only browse mode (0.9.92): filter the tree to the library, no part load.     if symbol_name.is_empty() {-        let (_, trace, nav_mech) = navigate_tree(frame, library_name, Verify::Never);-        return json!({-            "success": true, "hwnd": frame, "libraryShown": library_name, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace,-            "output": format!("Symbol Editor open with the '{library_name}' library filtered and selected. No symbol was requested, so none was loaded into the canvas."),-            "via": "win32-library-browse",-            "_hint": "Library browse mode: the tree is filtered to the library. To load a part into the canvas, call again with symbolName too.",-        });+        let (ok, trace, nav_mech) = navigate_tree(frame, &library_filter(library_name), Verify::Never);+        let mut v = library_browse_reply(ok, &title_of(frame), "Symbol Editor", library_name, "No symbol was requested, so none was loaded into the canvas.");+        merge(&mut v, json!({"hwnd": frame, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace, "via": "win32-library-browse"}));+        if ok {+            v["_hint"] = json!("Library browse mode: the tree is filtered to the library. To load a part into the canvas, call again with symbolName too.");+        }+        return v;     }     let filter = if library_name.is_empty() { symbol_name.to_string() } else { format!("{library_name} {symbol_name}") };     let verify = Verify::Symbol { name: symbol_name, library: library_name };@@ -2262,13 +2302,13 @@ pub(crate) fn open_footprint_editor(state: &mut State, args: &Value) -> Value {         return json!({"success": true, "hwnd": frame, "mechanism": mechanism, "pathwaysTried": pathways, "output": if mechanism == "existing" { "Footprint Editor is already open.".to_string() } else { format!("Footprint Editor opened successfully ({mechanism}).") }});     }     if footprint_name.is_empty() {-        let (_, trace, nav_mech) = navigate_tree(frame, &library_name, Verify::Never);-        return json!({-            "success": true, "hwnd": frame, "libraryShown": library_name, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace,-            "output": format!("Footprint Editor open with the '{library_name}' library filtered and selected. No footprint was requested, so none was loaded into the canvas."),-            "via": "win32-library-browse",-            "_hint": "Library browse mode: the tree is filtered to the library. To load a part, call again with footprintName too.",-        });+        let (ok, trace, nav_mech) = navigate_tree(frame, &library_filter(&library_name), Verify::Never);+        let mut v = library_browse_reply(ok, &title_of(frame), "Footprint Editor", &library_name, "No footprint was requested, so none was loaded into the canvas.");+        merge(&mut v, json!({"hwnd": frame, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace, "via": "win32-library-browse"}));+        if ok {+            v["_hint"] = json!("Library browse mode: the tree is filtered to the library. To load a part, call again with footprintName too.");+        }+        return v;     }     let filter = if library_name.is_empty() { footprint_name.clone() } else { format!("{library_name} {footprint_name}") };     let verify = Verify::Footprint { name: &footprint_name, library: &library_name };@@ -3440,6 +3480,37 @@ fn enable_software_opengl(state: &mut State) -> Value {  #[cfg(test)] mod tests {+    #[test]+    fn a_library_browse_filters_on_the_library_itself() {+        assert_eq!(super::library_filter("Timer"), "Timer:");+        assert_eq!(super::library_filter(" Timer: "), "Timer:");+        assert_eq!(super::library_filter("My Parts"), "My Parts", "KiCad splits the filter on spaces");+    }++    #[test]+    fn a_library_browse_never_loads_a_part() {+        assert!(!super::loads_a_part(super::Verify::Never), "a browse with no part must stop at the filter");+        assert!(super::loads_a_part(super::Verify::Symbol { name: "R", library: "Device" }));+    }++    #[test]+    fn a_library_browse_says_what_the_canvas_still_shows() {+        let e = "Symbol Editor";+        let empty = super::library_browse_reply(true, "[no symbol loaded] \u{2014} Symbol Editor", e, "Tester", "None was loaded.");+        assert_eq!(empty["libraryShown"], "Tester");+        assert!(empty.get("canvasPart").is_none(), "{empty}");+        assert!(empty["output"].as_str().unwrap().contains("None was loaded."));+        let other = super::library_browse_reply(true, "MCU_Module:Flipper_Zero \u{2014} Symbol Editor", e, "Tester", "None was loaded.");+        assert_eq!(other["canvasPart"], "MCU_Module:Flipper_Zero");+        assert_eq!(other["canvasFromOtherLibrary"], true);+        assert!(other["output"].as_str().unwrap().contains("from before"), "{other}");+        let same = super::library_browse_reply(true, "Tester:BZT52C18Q \u{2014} Symbol Editor", e, "tester", "");+        assert_eq!(same["canvasFromOtherLibrary"], false);+        let bad = super::library_browse_reply(false, "", e, "Tester", "");+        assert_eq!(bad["success"], false);+        assert_eq!(bad["errorCode"], "library_filter_failed");+    }+     use super::*;     #[test]     fn viewer_selection_is_bound_to_source_and_refuses_ambiguity(){