1use std::cell::RefCell;
52use std::collections::HashMap;
53use std::f32::consts::FRAC_PI_2;
54use std::ops::Range;
55use std::rc::Rc;
56
57use gpui::{
58 AnyElement, App, Global, InteractiveElement, IntoElement, ListSizingBehavior, ParentElement,
59 RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement, Styled, Transformation,
60 UniformListScrollHandle, Window, div, prelude::FluentBuilder, px, radians, uniform_list,
61};
62use gpui_kit_assets::{Icon, icon};
63use gpui_kit_semantics::{NodeSpec, Role, Semantic};
64use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TextTone, Theme, TypeScale};
65use unicode_segmentation::UnicodeSegmentation;
66
67use crate::foundation::{Disableable, FocusRing, Ident, Pressable, Sizable, StyledExt, text};
68use crate::strings::{ActiveStrings, StringKey};
69
70type ToggleHandler = Rc<dyn Fn(SharedString, bool, &mut Window, &mut App)>;
71type SelectHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
72
73#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum JsonValue {
77 Null,
78 Bool(bool),
79 Number(SharedString),
81 String(SharedString),
82 Array(Vec<JsonValue>),
83 Object(Vec<(SharedString, JsonValue)>),
85 Redacted(SharedString),
88}
89
90impl JsonValue {
91 pub fn number(text: impl Into<SharedString>) -> Self {
92 Self::Number(text.into())
93 }
94
95 pub fn string(text: impl Into<SharedString>) -> Self {
96 Self::String(text.into())
97 }
98
99 pub fn array(items: impl IntoIterator<Item = JsonValue>) -> Self {
100 Self::Array(items.into_iter().collect())
101 }
102
103 pub fn object(members: impl IntoIterator<Item = (impl Into<SharedString>, JsonValue)>) -> Self {
104 Self::Object(
105 members
106 .into_iter()
107 .map(|(key, value)| (key.into(), value))
108 .collect(),
109 )
110 }
111
112 pub fn redacted(shape: impl Into<SharedString>) -> Self {
114 Self::Redacted(shape.into())
115 }
116
117 pub fn redacted_from(value: &JsonValue, cx: &App) -> Self {
123 let strings = cx.strings();
124 let shape = match value {
125 JsonValue::String(text) => strings.format(
126 StringKey::DescriptionCharacters,
127 &[&text.graphemes(true).count().to_string()],
128 ),
129 JsonValue::Object(members) => {
130 strings.format(StringKey::JsonShapeEntries, &[&members.len().to_string()])
131 }
132 JsonValue::Array(items) => {
133 strings.format(StringKey::JsonShapeItems, &[&items.len().to_string()])
134 }
135 _ => strings.text(StringKey::JsonShapeValue),
136 };
137 Self::Redacted(shape)
138 }
139
140 pub fn kind(&self) -> ValueKind {
141 match self {
142 Self::Null => ValueKind::Null,
143 Self::Bool(_) => ValueKind::Bool,
144 Self::Number(_) => ValueKind::Number,
145 Self::String(_) => ValueKind::String,
146 Self::Array(_) => ValueKind::Array,
147 Self::Object(_) => ValueKind::Object,
148 Self::Redacted(_) => ValueKind::Redacted,
149 }
150 }
151
152 fn member_count(&self) -> usize {
156 match self {
157 Self::Array(items) => items.len(),
158 Self::Object(members) => members.len(),
159 _ => 0,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum ValueKind {
167 Null,
168 Bool,
169 Number,
170 String,
171 Array,
172 Object,
173 Redacted,
174}
175
176impl ValueKind {
177 fn published(self, value: &JsonValue) -> SharedString {
181 match value {
182 JsonValue::Null => SharedString::new_static("null"),
183 JsonValue::Bool(true) => SharedString::new_static("true"),
184 JsonValue::Bool(false) => SharedString::new_static("false"),
185 JsonValue::Number(text) => text.clone(),
186 JsonValue::String(text) => text.clone(),
187 JsonValue::Array(items) if items.is_empty() => SharedString::new_static("empty array"),
188 JsonValue::Array(_) => SharedString::new_static("array"),
189 JsonValue::Object(members) if members.is_empty() => {
190 SharedString::new_static("empty object")
191 }
192 JsonValue::Object(_) => SharedString::new_static("object"),
193 JsonValue::Redacted(_) => SharedString::new_static("withheld"),
196 }
197 }
198}
199
200#[derive(Debug, Clone)]
202struct Line {
203 path: SharedString,
208 label: SharedString,
210 kind: ValueKind,
211 shown: SharedString,
213 shape: Option<SharedString>,
215 published: SharedString,
216 level: u32,
217 open: bool,
218 has_members: bool,
219 parent: Option<SharedString>,
220 first_member: Option<SharedString>,
221}
222
223fn escape(token: &str) -> String {
226 token.replace('~', "~0").replace('/', "~1")
227}
228
229fn join(parent: &str, token: &str) -> SharedString {
230 if parent.is_empty() {
231 SharedString::from(escape(token))
232 } else {
233 SharedString::from(format!("{parent}/{}", escape(token)))
234 }
235}
236
237fn shown_text(value: &JsonValue) -> SharedString {
243 match value {
244 JsonValue::Null => SharedString::new_static("null"),
245 JsonValue::Bool(true) => SharedString::new_static("true"),
246 JsonValue::Bool(false) => SharedString::new_static("false"),
247 JsonValue::Number(text) => text.clone(),
248 JsonValue::String(text) => SharedString::from(format!("\"{text}\"")),
249 JsonValue::Array(items) if items.is_empty() => SharedString::new_static("[]"),
250 JsonValue::Array(_) => SharedString::new_static("[…]"),
251 JsonValue::Object(members) if members.is_empty() => SharedString::new_static("{}"),
252 JsonValue::Object(_) => SharedString::new_static("{…}"),
253 JsonValue::Redacted(_) => SharedString::new_static("••••••••"),
254 }
255}
256
257fn flatten(
258 value: &JsonValue,
259 path: SharedString,
260 label: SharedString,
261 level: u32,
262 parent: Option<&SharedString>,
263 expanded: &[SharedString],
264 out: &mut Vec<Line>,
265) {
266 let has_members = value.member_count() > 0;
267 let open = has_members && expanded.contains(&path);
268 let first_member = match value {
269 JsonValue::Object(members) => members
270 .first()
271 .map(|(key, _)| join(path.as_ref(), key.as_ref())),
272 JsonValue::Array(items) if !items.is_empty() => Some(join(path.as_ref(), "0")),
273 _ => None,
274 };
275 out.push(Line {
276 path: path.clone(),
277 label,
278 kind: value.kind(),
279 shown: shown_text(value),
280 shape: match value {
281 JsonValue::Redacted(shape) => Some(shape.clone()),
282 _ => None,
283 },
284 published: value.kind().published(value),
285 level,
286 open,
287 has_members,
288 parent: parent.cloned(),
289 first_member,
290 });
291 if !open {
292 return;
293 }
294 match value {
295 JsonValue::Object(members) => {
296 for (key, member) in members {
297 flatten(
298 member,
299 join(path.as_ref(), key.as_ref()),
300 key.clone(),
301 level + 1,
302 Some(&path),
303 expanded,
304 out,
305 );
306 }
307 }
308 JsonValue::Array(items) => {
309 for (index, item) in items.iter().enumerate() {
310 let token = index.to_string();
311 flatten(
312 item,
313 join(path.as_ref(), &token),
314 SharedString::from(token),
315 level + 1,
316 Some(&path),
317 expanded,
318 out,
319 );
320 }
321 }
322 _ => {}
323 }
324}
325
326#[derive(IntoElement)]
328pub struct JsonView {
329 ident: Ident,
330 value: JsonValue,
331 root_label: Option<SharedString>,
332 expanded: Vec<SharedString>,
333 selected: Option<SharedString>,
334 visible_rows: Option<usize>,
335 row_height: Option<f32>,
336 size: ControlSize,
337 disabled: bool,
338 on_toggle: Option<ToggleHandler>,
339 on_select: Option<SelectHandler>,
340}
341
342impl std::fmt::Debug for JsonView {
343 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 formatter
345 .debug_struct("JsonView")
346 .field("ident", &self.ident)
347 .field("kind", &self.value.kind())
348 .field("expanded", &self.expanded)
349 .field("selected", &self.selected)
350 .field("disabled", &self.disabled)
351 .finish()
352 }
353}
354
355impl JsonView {
356 pub fn new(ident: impl Into<Ident>, value: JsonValue) -> Self {
357 Self {
358 ident: ident.into(),
359 value,
360 root_label: None,
361 expanded: Vec::new(),
362 selected: None,
363 visible_rows: None,
364 row_height: None,
365 size: ControlSize::Md,
366 disabled: false,
367 on_toggle: None,
368 on_select: None,
369 }
370 }
371
372 pub fn root_label(mut self, label: impl Into<SharedString>) -> Self {
376 self.root_label = Some(label.into());
377 self
378 }
379
380 pub fn expanded(mut self, paths: impl IntoIterator<Item = SharedString>) -> Self {
383 self.expanded = paths.into_iter().collect();
384 self
385 }
386
387 pub fn expanded_paths<S: AsRef<str>>(mut self, paths: &[S]) -> Self {
388 self.expanded = paths
389 .iter()
390 .map(|path| SharedString::from(path.as_ref().to_string()))
391 .collect();
392 self
393 }
394
395 pub fn selected(mut self, path: impl Into<SharedString>) -> Self {
396 self.selected = Some(path.into());
397 self
398 }
399
400 pub fn visible_rows(mut self, rows: usize) -> Self {
404 self.visible_rows = Some(rows);
405 self
406 }
407
408 pub fn row_height(mut self, height: f32) -> Self {
409 self.row_height = Some(height);
410 self
411 }
412
413 pub fn on_toggle(
416 mut self,
417 handler: impl Fn(SharedString, bool, &mut Window, &mut App) + 'static,
418 ) -> Self {
419 self.on_toggle = Some(Rc::new(handler));
420 self
421 }
422
423 pub fn on_select(
424 mut self,
425 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
426 ) -> Self {
427 self.on_select = Some(Rc::new(handler));
428 self
429 }
430
431 pub fn disclosed_paths(&self, cx: &App) -> Vec<SharedString> {
435 self.lines(cx)
436 .into_iter()
437 .map(|line| line.path)
438 .collect::<Vec<_>>()
439 }
440
441 fn lines(&self, cx: &App) -> Vec<Line> {
442 let mut lines = Vec::new();
443 match &self.value {
444 JsonValue::Object(members) => {
445 for (key, member) in members {
446 flatten(
447 member,
448 join("", key.as_ref()),
449 key.clone(),
450 1,
451 None,
452 &self.expanded,
453 &mut lines,
454 );
455 }
456 }
457 JsonValue::Array(items) => {
458 for (index, item) in items.iter().enumerate() {
459 let token = index.to_string();
460 flatten(
461 item,
462 join("", &token),
463 SharedString::from(token),
464 1,
465 None,
466 &self.expanded,
467 &mut lines,
468 );
469 }
470 }
471 scalar => {
472 let label = self
473 .root_label
474 .clone()
475 .unwrap_or_else(|| cx.strings().text(StringKey::JsonRootValue));
476 flatten(
477 scalar,
478 SharedString::default(),
479 label,
480 1,
481 None,
482 &self.expanded,
483 &mut lines,
484 );
485 }
486 }
487 lines
488 }
489
490 fn row_ident(&self, path: &SharedString) -> Ident {
496 if path.is_empty() {
497 self.ident.child("value")
498 } else {
499 self.ident.child(path.as_ref())
500 }
501 }
502}
503
504impl Disableable for JsonView {
505 fn disabled(mut self, disabled: bool) -> Self {
506 self.disabled = disabled;
507 self
508 }
509}
510
511impl Sizable for JsonView {
512 fn control_size(mut self, size: ControlSize) -> Self {
513 self.size = size;
514 self
515 }
516}
517
518enum Move {
520 Select(SharedString),
521 Toggle(SharedString, bool),
522}
523
524fn keystroke_move(key: &str, lines: &[Line], selected: Option<&SharedString>) -> Option<Move> {
528 let at = lines
529 .iter()
530 .position(|line| Some(&line.path) == selected)
531 .filter(|_| selected.is_some());
532 match key {
533 "up" | "down" => {
534 let next = match (key, at) {
535 ("down", Some(at)) => at + 1,
536 ("down", None) => 0,
537 ("up", Some(at)) => at.checked_sub(1)?,
538 _ => lines.len().checked_sub(1)?,
539 };
540 lines.get(next).map(|line| Move::Select(line.path.clone()))
541 }
542 "home" => lines.first().map(|line| Move::Select(line.path.clone())),
543 "end" => lines.last().map(|line| Move::Select(line.path.clone())),
544 "right" => {
545 let line = lines.get(at?)?;
546 if line.has_members && !line.open {
547 Some(Move::Toggle(line.path.clone(), true))
548 } else {
549 line.first_member
550 .clone()
551 .filter(|_| line.open)
552 .map(Move::Select)
553 }
554 }
555 "left" => {
556 let line = lines.get(at?)?;
557 if line.has_members && line.open {
558 Some(Move::Toggle(line.path.clone(), false))
559 } else {
560 line.parent.clone().map(Move::Select)
561 }
562 }
563 _ => None,
564 }
565}
566
567impl RenderOnce for JsonView {
568 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
569 let theme = cx.theme().clone();
570 let metrics = theme.control.get(self.size);
571 let row_height = self.row_height.unwrap_or(metrics.height);
572 let lines = Rc::new(self.lines(cx));
573 let count = lines.len();
574 let scroll = scroll_handle(&self.ident, cx);
575 let view = Rc::new(self);
576
577 let owner = Rc::clone(&view);
578 let source = Rc::clone(&lines);
579 let row_theme = theme.clone();
580 let rows = uniform_list(
581 view.ident.child("rows").element_id(),
582 count,
583 move |range: Range<usize>, window, cx| {
584 range
585 .filter_map(|index| source.get(index).cloned())
586 .map(|line| {
587 row_element(
588 &owner,
589 &row_theme,
590 row_height,
591 metrics.icon_size,
592 &line,
593 window,
594 cx,
595 )
596 })
597 .collect::<Vec<_>>()
598 },
599 )
600 .track_scroll(&scroll)
601 .w_full()
602 .with_sizing_behavior(if view.visible_rows.is_some() {
603 ListSizingBehavior::Auto
604 } else {
605 ListSizingBehavior::Infer
606 })
607 .when_some(view.visible_rows, |element, rows| {
608 element.h(px(row_height * rows as f32))
609 });
610
611 let mut container = div()
612 .id(view.ident.element_id())
613 .column()
614 .w_full()
615 .font_family(theme.typography.mono.clone())
616 .child(rows);
617
618 if !view.disabled && (view.on_select.is_some() || view.on_toggle.is_some()) {
619 let selected = view.selected.clone();
620 let select = view.on_select.clone();
621 let toggle = view.on_toggle.clone();
622 let lines = Rc::clone(&lines);
623 let scroll = scroll.clone();
624 container = container.on_key_down(move |event, window, cx| {
625 let Some(next) =
626 keystroke_move(event.keystroke.key.as_str(), &lines, selected.as_ref())
627 else {
628 return;
629 };
630 match next {
631 Move::Select(path) => {
632 if Some(&path) == selected.as_ref() {
633 return;
634 }
635 if let Some(index) = lines.iter().position(|line| line.path == path) {
638 scroll.scroll_to_item(index, ScrollStrategy::Nearest);
639 window.refresh();
640 }
641 let Some(handler) = select.as_ref() else {
642 return;
643 };
644 handler(path, window, cx);
645 }
646 Move::Toggle(path, open) => {
647 let Some(handler) = toggle.as_ref() else {
648 return;
649 };
650 handler(path, open, window, cx);
651 }
652 }
653 cx.stop_propagation();
654 });
655 }
656
657 container.semantic_in(
658 cx,
659 NodeSpec::new(view.ident.semantic_id(), Role::Tree).value(count.to_string()),
660 )
661 }
662}
663
664fn row_element(
665 view: &JsonView,
666 theme: &Theme,
667 height: f32,
668 icon_size: f32,
669 line: &Line,
670 _window: &mut Window,
671 cx: &mut App,
672) -> AnyElement {
673 let ident = view.row_ident(&line.path);
674 let selected = view.selected.as_ref() == Some(&line.path);
675 let selectable = !view.disabled && view.on_select.is_some();
676 let toggleable = !view.disabled && line.has_members && view.on_toggle.is_some();
677 let value_color = match line.kind {
678 ValueKind::Null | ValueKind::Redacted => theme.colors.text_faint,
681 _ => theme.colors.text,
682 };
683
684 let chevron = line.has_members.then(|| {
685 let toggle = ident.child("toggle");
686 let mut glyph = div()
687 .id(toggle.element_id())
688 .row()
689 .flex_none()
690 .size(px(icon_size))
691 .child(
692 icon(Icon::AltArrowRight)
693 .size(px(icon_size))
694 .text_color(theme.colors.text_muted)
695 .when(line.open, |glyph| {
696 glyph.with_transformation(Transformation::rotate(radians(FRAC_PI_2)))
697 }),
698 )
699 .when(toggleable, |element| {
700 element
701 .cursor_pointer()
702 .tab_index(0)
703 .pressable(cx)
704 .focus_ring(theme)
705 });
706
707 if let (true, Some(handler)) = (toggleable, view.on_toggle.clone()) {
708 let path = line.path.clone();
709 let open = line.open;
710 glyph = glyph.on_click(move |_, window, cx| {
711 handler(path.clone(), !open, window, cx);
712 cx.stop_propagation();
715 });
716 }
717
718 glyph.semantic_in(
719 cx,
720 NodeSpec::new(toggle.semantic_id(), Role::Button)
721 .parent(ident.semantic_id())
722 .text(line.label.clone())
723 .expanded(line.open)
724 .disabled(!toggleable),
725 )
726 });
727
728 let mut row = div()
729 .id(ident.element_id())
730 .row()
731 .w_full()
732 .h(px(height))
733 .pr(px(theme.space(Space::Sm)))
734 .pl(px(theme.space(Space::Sm)
735 + line.level.saturating_sub(1) as f32
736 * theme.space(Space::Md)))
737 .gap(px(theme.space(Space::Xs)))
738 .when(selected, |element| element.bg(theme.colors.selected))
739 .when(view.disabled, |element| {
740 element.opacity(theme.opacity.disabled)
741 })
742 .when(selectable, |element| {
743 element
744 .cursor_pointer()
745 .tab_index(0)
746 .pressable(cx)
747 .when(!selected, |element| {
748 element.hover(|style| style.bg(theme.colors.hover.opacity(0.3)))
749 })
750 .focus_ring(theme)
751 })
752 .children(chevron)
753 .when(!line.has_members, |element| {
756 element.child(div().flex_none().size(px(icon_size)))
757 })
758 .child(
759 text(theme, TypeScale::Code, line.label.clone())
760 .flex_none()
761 .text_tone(theme, TextTone::Muted),
762 )
763 .child(
764 text(theme, TypeScale::Code, line.shown.clone())
765 .flex_1()
766 .overflow_hidden()
767 .text_color(value_color),
768 );
769
770 if let Some(shape) = line.shape.clone() {
773 row = row.child(
774 div()
775 .flex_none()
776 .row()
777 .gap(px(theme.space(Space::Xs)))
778 .child(
779 text(
780 theme,
781 TypeScale::Caption,
782 cx.strings().text(StringKey::JsonWithheld),
783 )
784 .text_tone(theme, TextTone::Muted),
785 )
786 .child(text(theme, TypeScale::Code, shape).text_tone(theme, TextTone::Faint)),
787 );
788 }
789
790 if let (true, Some(handler)) = (selectable, view.on_select.clone()) {
791 let path = line.path.clone();
792 row = row.on_click(move |_, window, cx| handler(path.clone(), window, cx));
793 }
794
795 let mut spec = NodeSpec::new(ident.semantic_id(), Role::TreeItem)
796 .parent(match &line.parent {
797 Some(parent) => view.row_ident(parent).semantic_id(),
798 None => view.ident.semantic_id(),
799 })
800 .text(line.label.clone())
801 .value(line.published.clone())
802 .selected(selected)
803 .disabled(view.disabled)
804 .level(line.level);
805 if line.has_members {
808 spec = spec.expanded(line.open);
809 }
810
811 row.semantic_in(cx, spec).into_any_element()
812}
813
814#[derive(Default)]
815struct ScrollHandles(RefCell<HashMap<SharedString, UniformListScrollHandle>>);
816
817impl Global for ScrollHandles {}
818
819fn scroll_handle(ident: &Ident, cx: &mut App) -> UniformListScrollHandle {
822 if !cx.has_global::<ScrollHandles>() {
823 cx.set_global(ScrollHandles::default());
824 }
825 let mut handles = cx.global::<ScrollHandles>().0.borrow_mut();
826 handles.entry(ident.semantic_id()).or_default().clone()
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 fn document() -> JsonValue {
834 JsonValue::object([
835 ("name", JsonValue::string("run")),
836 ("retries", JsonValue::number("3")),
837 ("cursor", JsonValue::Null),
838 ("labels", JsonValue::object(Vec::<(&str, JsonValue)>::new())),
839 (
840 "steps",
841 JsonValue::array([JsonValue::string("plan"), JsonValue::string("apply")]),
842 ),
843 ])
844 }
845
846 fn lines(expanded: &[&str]) -> Vec<Line> {
847 let expanded: Vec<SharedString> = expanded
848 .iter()
849 .map(|path| SharedString::from(path.to_string()))
850 .collect();
851 let mut out = Vec::new();
852 let JsonValue::Object(members) = document() else {
853 unreachable!()
854 };
855 for (key, member) in &members {
856 flatten(
857 member,
858 join("", key.as_ref()),
859 key.clone(),
860 1,
861 None,
862 &expanded,
863 &mut out,
864 );
865 }
866 out
867 }
868
869 #[test]
870 fn a_shut_value_discloses_nothing() {
871 let shut = lines(&[]);
872 let paths: Vec<&str> = shut.iter().map(|line| line.path.as_ref()).collect();
873 assert_eq!(
874 paths,
875 vec!["name", "retries", "cursor", "labels", "steps"],
876 "a shut array must not lay out its items"
877 );
878 }
879
880 #[test]
881 fn an_empty_object_offers_no_disclosure() {
882 let labels = lines(&[]);
883 let empty = labels
884 .iter()
885 .find(|line| line.path.as_ref() == "labels")
886 .expect("present");
887 assert!(!empty.has_members);
888 assert_eq!(empty.published.as_ref(), "empty object");
889 }
890
891 #[test]
892 fn a_key_containing_a_slash_stays_one_level() {
893 let value = JsonValue::object([("a/b", JsonValue::object([("c", JsonValue::Bool(true))]))]);
894 let JsonValue::Object(members) = &value else {
895 unreachable!()
896 };
897 let mut out = Vec::new();
898 flatten(
899 &members[0].1,
900 join("", members[0].0.as_ref()),
901 members[0].0.clone(),
902 1,
903 None,
904 &[SharedString::from("a~1b")],
905 &mut out,
906 );
907 assert_eq!(out[0].path.as_ref(), "a~1b");
908 assert_eq!(out[1].path.as_ref(), "a~1b/c");
909 }
910
911 #[test]
912 fn right_opens_a_shut_value_and_then_descends() {
913 let shut = lines(&[]);
914 let steps = SharedString::from("steps");
915 match keystroke_move("right", &shut, Some(&steps)) {
916 Some(Move::Toggle(path, next)) => {
917 assert_eq!(path.as_ref(), "steps");
918 assert!(next);
919 }
920 _ => panic!("right must open a shut value"),
921 }
922 let open = lines(&["steps"]);
923 match keystroke_move("right", &open, Some(&steps)) {
924 Some(Move::Select(path)) => assert_eq!(path.as_ref(), "steps/0"),
925 _ => panic!("right must descend into an open value"),
926 }
927 }
928
929 #[test]
930 fn a_move_stops_at_the_ends() {
931 let shut = lines(&[]);
932 let last = SharedString::from("steps");
933 assert!(keystroke_move("down", &shut, Some(&last)).is_none());
934 let first = SharedString::from("name");
935 assert!(keystroke_move("up", &shut, Some(&first)).is_none());
936 }
937}