diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 2fdf31fef..c4807d701 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -2114,6 +2114,45 @@ fn sample_plugin_tool_window_with_a_tree() { } } +/// The tables fixture sorts its own rows: a `table` body sent through `other` +/// opts the headers in, and a header click leaves as an `other` view-event +/// (kind `sort`) the plugin decodes and answers with the rows in the asked +/// order (plugins/README.md §10: released types stay, `other` carries new +/// cases). +#[test] +fn sortable_plugin_table_round_trips_sort_events() { + let f = fixture(Limits::default()); + let info = f.install(&sample("tables")); + assert_eq!(info.status, PluginStatus::Inactive, "{:?}", info.error); + + let table = |f: &Fixture| match f.host.render_view("tables", "tables", None, f.inv()).unwrap().body { + ide_plugin::ViewBody::Table(t) => t, + other => panic!("{other:?}"), + }; + // No sort yet: headers are clickable, rows in the plugin's own order. + let t = table(&f); + assert_eq!((t.columns.clone(), t.total_rows), (vec!["ext".to_owned(), "files".into()], 4)); + assert_eq!((t.sortable, t.sort), (true, None), "a `table` through `other` opts the headers in"); + assert_eq!(t.rows.iter().map(|r| r[0].clone()).collect::>(), ["rs", "md", "java", "toml"]); + + f.take_events(); + let sort = |f: &Fixture, descending| { + f.host.view_event("tables", "tables", ide_plugin::ViewEvent::Sort { column: 1, descending }, f.inv()).unwrap(); + assert!(f.take_events().into_iter().any( + |(_, e)| matches!(e, PluginEvent::ViewChanged { ref plugin_id, ref view } if plugin_id == "tables" && view == "tables") + )); + table(f) + }; + + // The plugin reports the sort it ordered by, with the rows to match. + let t = sort(&f, false); + assert_eq!(t.sort, Some(ide_plugin::TableSort { column: 1, descending: false })); + assert_eq!(t.rows.iter().map(|r| r[1].clone()).collect::>(), ["3", "9", "12", "20"]); + let t = sort(&f, true); + assert_eq!(t.sort, Some(ide_plugin::TableSort { column: 1, descending: true })); + assert_eq!(t.rows.iter().map(|r| r[1].clone()).collect::>(), ["20", "12", "9", "3"]); +} + /// Regex Tester: an `html` body its script talks to; the plugin answers in /// view messages, in UTF-16 offsets. #[test] diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index 902a97d96..aabc1d6ee 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -12,8 +12,8 @@ use crate::types::{ RepositoryStatus, StatusItem, SyntaxFold, SyntaxSymbol, SyntaxToken, WorkbenchEvent, }; use crate::types::{ - FieldValue, ServiceLog, ServiceLogToggle, ServicePane, ServiceTab, View, ViewBody, ViewButton, ViewEvent, - ViewField, ViewListItem, ViewTable, ViewTreeItem, Viewport, + FieldValue, ServiceLog, ServiceLogToggle, ServicePane, ServiceTab, TableSort, View, ViewBody, ViewButton, + ViewEvent, ViewField, ViewListItem, ViewTable, ViewTreeItem, Viewport, }; use crate::wit_host::{ bindings, commit_checks_bindings, details_bindings, hover_bindings, indexing_bindings, inlay_bindings, @@ -1353,12 +1353,54 @@ fn service_tab_from_wit(t: wit_dt::Tab) -> ServiceTab { ServiceTab { id: clip(t.id, 64), label: clip(t.label, MAX_TITLE), body } } +/// A `table` body carried by the `other` case (the sortable variant, plugins/ +/// README.md §10): the usual table fields as JSON plus `sort`, the column the +/// plugin orders by and the direction, or null. Anything unparsable degrades +/// like a body this host doesn't know. +fn sortable_table_from_json(data: &str) -> Option { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct Json { + #[serde(default)] + columns: Vec, + #[serde(default)] + total_rows: u32, + #[serde(default)] + first_row: u32, + #[serde(default)] + rows: Vec>, + #[serde(default)] + editable: bool, + #[serde(default)] + sort: Option, + } + let json: Json = serde_json::from_str(data).ok()?; + Some(ViewTable { + columns: json.columns.into_iter().take(1000).map(|c| clip(c, MAX_TITLE)).collect(), + total_rows: json.total_rows, + first_row: json.first_row, + rows: json + .rows + .into_iter() + .take(MAX_TABLE_ROWS as usize) + .map(|r| r.into_iter().take(1000).map(|c| clip(c, MAX_CELL)).collect()) + .collect(), + editable: json.editable, + sortable: true, + sort: json.sort, + }) +} + fn view_from_wit(v: wit_vt::View) -> View { let body = match v.body { wit_vt::Body::Message(m) => ViewBody::Message(clip(m, 16 << 10)), - wit_vt::Body::Other(e) => { - ViewBody::Message(format!("This view needs a newer version of OpenIDEA ({}).", clip(e.kind, 64))) - } + wit_vt::Body::Other(e) => match e.kind.as_str() { + "table" => match sortable_table_from_json(&e.data) { + Some(t) => ViewBody::Table(t), + None => ViewBody::Message("This view needs a newer version of OpenIDEA (table).".into()), + }, + _ => ViewBody::Message(format!("This view needs a newer version of OpenIDEA ({}).", clip(e.kind, 64))), + }, wit_vt::Body::Tree(items) => ViewBody::Tree( items .into_iter() @@ -1417,6 +1459,9 @@ fn view_from_wit(v: wit_vt::View) -> View { .map(|r| r.into_iter().take(1000).map(|c| clip(c, MAX_CELL)).collect()) .collect(), editable: t.editable, + // The plain body predates sortable headers; `other` kind `table` opts in. + sortable: false, + sort: None, }), wit_vt::Body::Html(h) => ViewBody::Html(clip(h, MAX_HTML)), }; @@ -1450,6 +1495,12 @@ fn event_to_wit_view(e: ViewEvent) -> wit_vt::ViewEvent { ViewEvent::InsertRow { before } => W::InsertRow(before), ViewEvent::DeleteRows { rows } => W::DeleteRows(rows), ViewEvent::Message { text } => W::Message(text), + // A sortable table's header click, as an `other` view-event (plugins/ + // README.md §10): the SDKs decode the kinds they know. + ViewEvent::Sort { column, descending } => W::Other(wit_types::Extension { + kind: "sort".into(), + data: format!(r#"{{"column":{column},"descending":{descending}}}"#), + }), } } @@ -1495,4 +1546,41 @@ mod tests { let ViewBody::Form(fields) = form.body else { panic!("a form") }; assert_eq!(fields.iter().map(|f| f.key.as_str()).collect::>(), ["name"], "unknown fields are left out"); } + + #[test] + fn a_sortable_table_arrives_through_other() { + let view = |data: &str| wit_vt::View { + toolbar: Vec::new(), + body: wit_vt::Body::Other(wit_types::Extension { kind: "table".into(), data: data.into() }), + status: None, + }; + let shown = view_from_wit(view( + r#"{"columns":["ext","files"],"totalRows":3,"firstRow":1,"rows":[["rs","2"],["md","1"]], + "editable":false,"sort":{"column":1,"descending":true}}"#, + )); + let ViewBody::Table(t) = shown.body else { panic!("{shown:?}") }; + assert_eq!(t.columns, ["ext".to_owned(), "files".into()]); + assert_eq!((t.total_rows, t.first_row, t.editable), (3, 1, false)); + assert!(t.sortable, "a `table` through `other` opts the headers in"); + assert_eq!(t.sort, Some(TableSort { column: 1, descending: true })); + + // No sort yet: still clickable headers, just no arrow. + let fresh = view_from_wit(view( + r#"{"columns":["a"],"totalRows":0,"firstRow":0,"rows":[],"editable":false,"sort":null}"#, + )); + let ViewBody::Table(t) = fresh.body else { panic!("{fresh:?}") }; + assert!(t.sortable); + assert_eq!(t.sort, None); + + // Broken JSON degrades like any body the host doesn't know. + let broken = view_from_wit(view("{not json")); + assert!(matches!(broken.body, ViewBody::Message(_)), "{broken:?}"); + } + + #[test] + fn a_sort_click_leaves_as_an_other_extension() { + let wit = event_to_wit_view(ViewEvent::Sort { column: 2, descending: true }); + let wit_vt::ViewEvent::Other(e) = wit else { panic!("{wit:?}") }; + assert_eq!((e.kind.as_str(), e.data.as_str()), ("sort", r#"{"column":2,"descending":true}"#)); + } } diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index a3106f9c7..6287b8e88 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -526,6 +526,14 @@ pub struct ViewField { pub value: FieldValue, } +/// A sortable table's order: the column and the direction. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TableSort { + pub column: u32, + pub descending: bool, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ViewTable { @@ -534,6 +542,10 @@ pub struct ViewTable { pub first_row: u32, pub rows: Vec>, pub editable: bool, + /// The plugin opted its headers in to click-to-sort (a `table` body through `other`). + pub sortable: bool, + /// The sort the plugin orders by (none: no arrow yet). + pub sort: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] @@ -614,4 +626,7 @@ pub enum ViewEvent { InsertRow { before: u32 }, DeleteRows { rows: Vec }, Message { text: String }, + // A sortable table's header click: the column and the direction (leaves + // as an `other` view-event, kind `sort`). See `TableSort`. + Sort { column: u32, descending: bool }, } diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm index e20cb7abb..8f307470c 100644 Binary files a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index 8cc6e9802..a1ba8694c 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -19b672d86ff84edbbdb32ba06aefba08344e28b00310c09260d935a6fd2d6d92 +6c6c9e25269f0c7d11aae8fd6c7ff66f457c8883b4f9ff19ae75f96c445ca3ff diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index 8be66269d..ba88bd79d 100644 Binary files a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 60afeff3e..a92345f28 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -5916e03cba741eea838c99757a50b86ecae020a75dc856d0301a5afcd4beb437 +21fefee002afa17326ad22d9ff7e099500d07ab595035be3c6001ac15f4a08b0 diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm index 6d75c5c98..efcad8abe 100644 Binary files a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index a7b4f195a..a31a00868 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -cb9b6f980b976d04ff25289a7978fe6f0b1f77e99ac9bdbaa8fd0720e8306f29 +d7aa5bd1a9fefa6e6c35534dfd507d0f4aa872bb128ab6a01c35fb8abdc474b2 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm index 498d2e376..ed7a7ecb6 100644 Binary files a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 4cfbfba6c..9bf5f1550 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -e399272912a4664c9ce307ff128ce2059a2276e31b7806e1553870c4041c27b5 +44145d1953b8b1c5a5bd57a15f88d4b6e86c1d7f456a2cb24758d19aca9e152e diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm index b3939b9e4..6be609a4e 100644 Binary files a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index c6f214871..0b0a14901 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -5ad3afb9518e1e2d06e05b5ba5d7339b8aafa7d03502cad00bdacbfe73419775 +c324bb5591738282f63e90918be93cd7ac2a15f3c75fa105b60ead52a0504334 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index 4517c6904..68beedeee 100644 Binary files a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index 397386374..af98bd83d 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -93840a797e8f6d86e3d75432395370f19e6a632444d8441b346b9db6c32cd6f9 +95d0e2a562f204030c3b2128cd84825cc5d6b6e85514a75f70fb847a1170b355 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index 1da6e94d3..d6f2fc30e 100644 Binary files a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 05d898263..42ca65815 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -44a29b9c311e10a055625fc43940fb9ecd361639f8a11c992ac6817f39e95e36 +8687703fb1f2fb8d5ac187a6279a65d6cf1018a09262f6f5629bd4c903b26bdf diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm index be1795bb2..e85457d1d 100644 Binary files a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index 4588151c6..58b7a457b 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -36a33de65d506d7753722d38aea7576722fba583ef615de502a5e341b781ef18 +aa2d8d38e49d10c03f6c639ce45b2a26b772c9f106a70547e94a9d46d8eef662 diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm index 04ee398f0..36c158c77 100644 Binary files a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources index 00da4599c..4d75ad1d4 100644 --- a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources @@ -1 +1 @@ -a4f9b7a13a7c2aa1243359df865e659eb4b8f86fb6623d743dcc17988a10e183 +eb5a36b16bf3ac0f98c1a10d8c7199d47b39d7448a5f4da2a5b47a111b7fd694 diff --git a/crates/ide-plugin/tests/fixtures/tables/Cargo.lock b/crates/ide-plugin/tests/fixtures/tables/Cargo.lock new file mode 100644 index 000000000..d7cbc7276 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/tables/Cargo.lock @@ -0,0 +1,333 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-tables" +version = "0.1.0" +dependencies = [ + "openidea-plugin", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/ide-plugin/tests/fixtures/tables/Cargo.toml b/crates/ide-plugin/tests/fixtures/tables/Cargo.toml new file mode 100644 index 000000000..9ce0a429c --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/tables/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "openidea-plugin-tables" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" } + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm new file mode 100644 index 000000000..a3b8be47f Binary files /dev/null and b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources new file mode 100644 index 000000000..603bbcedb --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -0,0 +1 @@ +d7840fe3f2f2b21a2cfbdb7c874ebc3214d1a81e324853dd47b04d914434f050 diff --git a/crates/ide-plugin/tests/fixtures/tables/plugin.toml b/crates/ide-plugin/tests/fixtures/tables/plugin.toml new file mode 100644 index 000000000..b6f0ff762 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/tables/plugin.toml @@ -0,0 +1,18 @@ +# Test fixture: a tool window whose table the plugin sorts itself — the IDE +# makes the column headers clickable (`other` body, kind `table`) and sends +# `other` view-events, kind `sort`, which the plugin decodes and answers with +# the rows in the asked order. +id = "tables" +name = "Tables" +version = "0.1.0" +description = "Test fixture: a sortable table the plugin sorts itself." +authors = ["OpenIDEA contributors"] +engines.openidea = "^1.0" +wasm = "dist/plugin.wasm" +activation = ["on-view:tables"] + +[[contributes.views]] +id = "tables" +title = "Tables" +icon = "plugin" +location = "right" diff --git a/crates/ide-plugin/tests/fixtures/tables/src/lib.rs b/crates/ide-plugin/tests/fixtures/tables/src/lib.rs new file mode 100644 index 000000000..55f60c2cd --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/tables/src/lib.rs @@ -0,0 +1,71 @@ +//! Test fixture: a table of file-type counts the plugin sorts itself. The +//! headers are clickable ([`Body::sortable_table`], the plugin API's `other` +//! body, kind `table`), the clicks arrive as `other` view-events (kind +//! `sort`), and the rows render in the order the plugin was asked for. + +use openidea_plugin::host; +use openidea_plugin::views::{Body, Table, TableSort, View, ViewEvent, Viewport, Views}; +use openidea_plugin::{Error, Invocation, Plugin, register_plugin}; + +pub const VIEW: &str = "tables"; + +/// (extension, files) in no particular order, so sorting shows. +const ROWS: [(&str, &str); 4] = [("rs", "20"), ("md", "3"), ("java", "9"), ("toml", "12")]; + +#[derive(Default)] +pub struct Tables { + sort: Option, +} + +impl Tables { + /// The rows in the order `sort` asks for (a real plugin would sort its + /// whole table and page it; the fixture is small enough to sort inline). + fn rows(&self) -> Vec> { + let mut rows: Vec> = ROWS.iter().map(|(a, b)| vec![a.to_string(), b.to_string()]).collect(); + if let Some(sort) = self.sort { + let by_number = |r: &Vec| r[1].parse::().unwrap_or(0); + rows.sort_by(|a, b| { + let (x, y) = (by_number(a), by_number(b)); + if sort.descending { y.cmp(&x) } else { x.cmp(&y) } + }); + } + rows + } +} + +impl Plugin for Tables { + fn run_command(&mut self, id: &str, _ctx: &Invocation, _arg: Option<&str>) -> Result, Error> { + Err(Error::NotFound(format!("command {id}"))) + } +} + +impl Views for Tables { + fn render(&mut self, view: &str, _viewport: Option, _ctx: &Invocation) -> Result { + if view != VIEW { + return Err(Error::NotFound(view.into())); + } + let table = Table { + columns: vec!["ext".into(), "files".into()], + total_rows: ROWS.len() as u32, + first_row: 0, + rows: self.rows(), + editable: false, + }; + Ok(View::new(Body::sortable_table(&table, self.sort))) + } + + fn handle(&mut self, view: &str, event: ViewEvent, _ctx: &Invocation) -> Result<(), Error> { + if view != VIEW { + return Err(Error::NotFound(view.into())); + } + // The host may have encoded the click for a newer SDK: the typed + // helper reads the `other` kind `sort` this SDK knows. + if let Some(sort) = event.sort() { + self.sort = Some(sort); + host::refresh_view(VIEW); + } + Ok(()) + } +} + +register_plugin!(Tables, views); diff --git a/plugins/README.md b/plugins/README.md index fb7dcf292..6a1b306f3 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -603,6 +603,12 @@ collapse of items, form fields, table cells, inserted and deleted rows, and `html` messages. A tree is a flat list whose items name their `parent`; an `expandable` item gets its children when it is expanded. A table gets only the rows it shows (`viewport`): report `total_rows` and return those. +For a table the user can sort by clicking a header, build it with +`Body::sortable_table(&table, sort)`: it travels as a `table` through `other` +(so IDEs and plugins of any API version keep working), the IDE makes the +column headers clickable and asks for a column and direction with +`ViewEvent::sort()` — sort your rows and render again; the sort you report +shows as an arrow in the header. Bodies, field types and view events added by later APIs travel as `Other { kind, data }`: an IDE that doesn't know a body says it needs updating, and leaves out a field type it doesn't know. diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index 5016aa482..c9697cd98 100644 Binary files a/plugins/official/ai-completion/dist/plugin.wasm and b/plugins/official/ai-completion/dist/plugin.wasm differ diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index fd30e5f13..0267b45f3 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -103165fc45ab6a995cdddd10848ae81f3f137438c1981375e6741185813ccdb3 +43d43f7a317c6fb8d5cca1b2a07f0e8dae05be6f5fcfc002527a409c519deb4f diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index 5a638086a..5d0d07fff 100644 Binary files a/plugins/official/checkstyle/dist/plugin.wasm and b/plugins/official/checkstyle/dist/plugin.wasm differ diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 723aa76d1..b486e9c11 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -e0845829cfaa2d8f471ccfd8a41726757443df6591e1e06be9e499d6781c02fe +881d4ad1ac25784507c836c220fc09bdcdf45193f13538e928b8b3619b548552 diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index 9742d6f30..4f93512a0 100644 Binary files a/plugins/official/csv-editor/dist/plugin.wasm and b/plugins/official/csv-editor/dist/plugin.wasm differ diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index 068f8ec71..3093f1ac3 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -6490dcca17d4d87336c70bbf8fbfac90f711ed0079f43d16534703b179211069 +b029de3ddd2add1812b3071d407c59a97463a701b63c22232450b19a9f1d6557 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index f10f15761..b238f5dd6 100644 Binary files a/plugins/official/git-toolbox/dist/plugin.wasm and b/plugins/official/git-toolbox/dist/plugin.wasm differ diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 2cfcdf3b2..3a1d585bd 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -aa807e1f712896e0ffe1f16115bd8ab991b59df5fcae73e97b6a40d6c94aaf8f +4952cbef6094c109c3ca9637c8df7d47836b85632c450164f8f70b6b5f22d4e5 diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index 81f6a368d..26e53bd0f 100644 Binary files a/plugins/official/graphviz/dist/plugin.wasm and b/plugins/official/graphviz/dist/plugin.wasm differ diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index f678649c2..4a8c56d99 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -225ca906d21299fa45059c11d12468e6c3fc577aebb59441c51774a760e54a26 +34c03e490bea652f4e28fe2fb785dd9a2b0421f46c0015273793c14e074867f1 diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index 8c61f485f..16096aafa 100644 Binary files a/plugins/official/key-promoter/dist/plugin.wasm and b/plugins/official/key-promoter/dist/plugin.wasm differ diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 0c08f9c65..5f2f41137 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -189e378fc52adfb058243feef4e100835600a1d4751adc23a262d13e703ef5e9 +d18f6687bc32a7a06c32ee81dd6e0c88e3acefd29934c9fa7271179d94fed194 diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index 262c449b6..23d123cef 100644 Binary files a/plugins/official/license-header/dist/plugin.wasm and b/plugins/official/license-header/dist/plugin.wasm differ diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index 37e0ba50d..9d2cd989e 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -2b2e162de966b670918b1bbe937c34cf9bef014834fb38be820ec25f20b09159 +444dc979be1b8194631303c3ad6399b876ee9229d1a8554681406fd39164f604 diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index e845ea522..52c6fdb6a 100644 Binary files a/plugins/official/mybatis/dist/plugin.wasm and b/plugins/official/mybatis/dist/plugin.wasm differ diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index 0b3c74d15..9ea91bfca 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -6d13cd671dfed3c9c73be0e40b81cc5f757e4f8ebf6a9eecba0001fa8c314d47 +6f9412fce5e1a3d7453b0a1089d50544d49a3b1e8f022a5a1221ea597d2fd7dd diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index c35c9f149..6c56e4a05 100644 Binary files a/plugins/official/regex-tester/dist/plugin.wasm and b/plugins/official/regex-tester/dist/plugin.wasm differ diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index cfd5d4647..122edc5da 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -a941b31f326427d41cf3e62373bc421d086789a0ac1cb8c3647eafafe29c5834 +a861d2b792c06dbac0476fa0b99321296da52f792ff340b1fd25de3b0e6422de diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index e65107f6f..81ef22be5 100644 Binary files a/plugins/official/solidity/dist/plugin.wasm and b/plugins/official/solidity/dist/plugin.wasm differ diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index cc89ff3d5..a042f606c 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -86428f64ea11d4863794595f12a9c100423725981d66d6f2a862191a0951fecf +ccd7eab004cd2f767f6be21ce272f647e20695ceff8694293beff8ce8a06c646 diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index 16a22e679..619f9374d 100644 Binary files a/plugins/official/string-tools/dist/plugin.wasm and b/plugins/official/string-tools/dist/plugin.wasm differ diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index bfa8f5f9b..624214523 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -fc3b11c7b09f9a57c319af579efdd6f00d9ed51dccc6e86404c5164c6460d007 +8ae2a72ec23163926bac3914f4b27a429598982cd12f42f985df10e920942fdb diff --git a/sdk/rust/openidea-plugin/src/text.rs b/sdk/rust/openidea-plugin/src/text.rs index 7f34e968e..ae872bb10 100644 --- a/sdk/rust/openidea-plugin/src/text.rs +++ b/sdk/rust/openidea-plugin/src/text.rs @@ -30,6 +30,23 @@ pub fn position(text: &str, at: usize) -> Position { Position { line, character } } +/// The text as a JSON string literal's contents (`"`, `\`, control characters). +pub fn escape_json(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c < ' ' => out.push_str(&format!("\\u{:04x}", c as u32)), + c => out.push(c), + } + } + out +} + /// The text of a JSON string literal's contents (`\"`, `\\`, `\n`, `\uXXXX`). pub fn unescape_json(s: &str) -> String { let mut out = String::with_capacity(s.len()); diff --git a/sdk/rust/openidea-plugin/src/views.rs b/sdk/rust/openidea-plugin/src/views.rs index b8a693d54..9acf2b9bd 100644 --- a/sdk/rust/openidea-plugin/src/views.rs +++ b/sdk/rust/openidea-plugin/src/views.rs @@ -148,6 +148,15 @@ pub struct Table { pub editable: bool, } +/// The column a table's rows are ordered by, and the direction: what +/// [`Body::sortable_table`] reports and a header click asks for +/// ([`ViewEvent::sort`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TableSort { + pub column: u32, + pub descending: bool, +} + #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Body { @@ -167,6 +176,34 @@ pub enum Body { }, } +impl Body { + /// A table whose column headers the IDE makes clickable: the usual + /// [`Table`] fields plus the sort the plugin orders by (none: no arrow + /// yet), carried by the `other` body case (plugins/README.md §10), so + /// IDEs and plugins of any API version keep working. The plugin sorts its + /// own rows; a click asks for a column and direction with + /// [`ViewEvent::sort`], and [`crate::host::refresh_view`] shows the new + /// order. + pub fn sortable_table(table: &Table, sort: Option) -> Self { + let mut data = String::from(r#"{"columns":"#); + json_list(&mut data, table.columns.iter().map(String::as_str)); + data.push_str(&format!(",\"totalRows\":{},\"firstRow\":{},\"rows\":[", table.total_rows, table.first_row)); + for (i, row) in table.rows.iter().enumerate() { + if i > 0 { + data.push(','); + } + json_list(&mut data, row.iter().map(String::as_str)); + } + data.push_str(&format!("],\"editable\":{},\"sort\":", table.editable)); + match sort { + Some(s) => data.push_str(&format!(r#"{{"column":{},"descending":{}}}"#, s.column, s.descending)), + None => data.push_str("null"), + } + data.push('}'); + Self::Other { kind: "table".into(), data } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct View { pub toolbar: Vec