stack_frame_list.rs

  1use crate::{
  2    debugger_panel::DebugPanel,
  3    session::running::stack_frame_list::StackFrameEntry,
  4    tests::{active_debug_session_panel, init_test, init_test_workspace},
  5};
  6use dap::{
  7    StackFrame,
  8    requests::{StackTrace, Threads},
  9};
 10use editor::{Editor, ToPoint as _};
 11use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 12use project::{FakeFs, Project};
 13use serde_json::json;
 14use std::sync::Arc;
 15use task::LaunchConfig;
 16use unindent::Unindent as _;
 17use util::path;
 18
 19#[gpui::test]
 20async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
 21    executor: BackgroundExecutor,
 22    cx: &mut TestAppContext,
 23) {
 24    init_test(cx);
 25
 26    let fs = FakeFs::new(executor.clone());
 27
 28    let test_file_content = r#"
 29        import { SOME_VALUE } './module.js';
 30
 31        console.log(SOME_VALUE);
 32    "#
 33    .unindent();
 34
 35    let module_file_content = r#"
 36        export SOME_VALUE = 'some value';
 37    "#
 38    .unindent();
 39
 40    fs.insert_tree(
 41        path!("/project"),
 42        json!({
 43           "src": {
 44               "test.js": test_file_content,
 45               "module.js": module_file_content,
 46           }
 47        }),
 48    )
 49    .await;
 50
 51    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 52    let workspace = init_test_workspace(&project, cx).await;
 53    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 54
 55    let task = project.update(cx, |project, cx| {
 56        project.fake_debug_session(
 57            dap::DebugRequestType::Launch(LaunchConfig::default()),
 58            None,
 59            false,
 60            cx,
 61        )
 62    });
 63
 64    let session = task.await.unwrap();
 65    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 66
 67    client
 68        .on_request::<Threads, _>(move |_, _| {
 69            Ok(dap::ThreadsResponse {
 70                threads: vec![dap::Thread {
 71                    id: 1,
 72                    name: "Thread 1".into(),
 73                }],
 74            })
 75        })
 76        .await;
 77
 78    let stack_frames = vec![
 79        StackFrame {
 80            id: 1,
 81            name: "Stack Frame 1".into(),
 82            source: Some(dap::Source {
 83                name: Some("test.js".into()),
 84                path: Some(path!("/project/src/test.js").into()),
 85                source_reference: None,
 86                presentation_hint: None,
 87                origin: None,
 88                sources: None,
 89                adapter_data: None,
 90                checksums: None,
 91            }),
 92            line: 3,
 93            column: 1,
 94            end_line: None,
 95            end_column: None,
 96            can_restart: None,
 97            instruction_pointer_reference: None,
 98            module_id: None,
 99            presentation_hint: None,
100        },
101        StackFrame {
102            id: 2,
103            name: "Stack Frame 2".into(),
104            source: Some(dap::Source {
105                name: Some("module.js".into()),
106                path: Some(path!("/project/src/module.js").into()),
107                source_reference: None,
108                presentation_hint: None,
109                origin: None,
110                sources: None,
111                adapter_data: None,
112                checksums: None,
113            }),
114            line: 1,
115            column: 1,
116            end_line: None,
117            end_column: None,
118            can_restart: None,
119            instruction_pointer_reference: None,
120            module_id: None,
121            presentation_hint: None,
122        },
123    ];
124
125    client
126        .on_request::<StackTrace, _>({
127            let stack_frames = Arc::new(stack_frames.clone());
128            move |_, args| {
129                assert_eq!(1, args.thread_id);
130
131                Ok(dap::StackTraceResponse {
132                    stack_frames: (*stack_frames).clone(),
133                    total_frames: None,
134                })
135            }
136        })
137        .await;
138
139    client
140        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
141            reason: dap::StoppedEventReason::Pause,
142            description: None,
143            thread_id: Some(1),
144            preserve_focus_hint: None,
145            text: None,
146            all_threads_stopped: None,
147            hit_breakpoint_ids: None,
148        }))
149        .await;
150
151    cx.run_until_parked();
152
153    // trigger to load threads
154    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
155        session
156            .mode()
157            .as_running()
158            .unwrap()
159            .update(cx, |running_state, cx| {
160                running_state
161                    .session()
162                    .update(cx, |session, cx| session.threads(cx));
163            });
164    });
165
166    cx.run_until_parked();
167
168    // select first thread
169    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
170        session
171            .mode()
172            .as_running()
173            .unwrap()
174            .update(cx, |running_state, cx| {
175                running_state.select_current_thread(
176                    &running_state
177                        .session()
178                        .update(cx, |session, cx| session.threads(cx)),
179                    cx,
180                );
181            });
182    });
183
184    cx.run_until_parked();
185
186    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
187        let stack_frame_list = session
188            .mode()
189            .as_running()
190            .unwrap()
191            .update(cx, |state, _| state.stack_frame_list().clone());
192
193        stack_frame_list.update(cx, |stack_frame_list, cx| {
194            assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
195            assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
196        });
197    });
198
199    let shutdown_session = project.update(cx, |project, cx| {
200        project.dap_store().update(cx, |dap_store, cx| {
201            dap_store.shutdown_session(session.read(cx).session_id(), cx)
202        })
203    });
204
205    shutdown_session.await.unwrap();
206}
207
208#[gpui::test]
209async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
210    init_test(cx);
211
212    let fs = FakeFs::new(executor.clone());
213
214    let test_file_content = r#"
215        import { SOME_VALUE } './module.js';
216
217        console.log(SOME_VALUE);
218    "#
219    .unindent();
220
221    let module_file_content = r#"
222        export SOME_VALUE = 'some value';
223    "#
224    .unindent();
225
226    fs.insert_tree(
227        path!("/project"),
228        json!({
229           "src": {
230               "test.js": test_file_content,
231               "module.js": module_file_content,
232           }
233        }),
234    )
235    .await;
236
237    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
238    let workspace = init_test_workspace(&project, cx).await;
239    let _ = workspace.update(cx, |workspace, window, cx| {
240        workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
241    });
242
243    let cx = &mut VisualTestContext::from_window(*workspace, cx);
244
245    let task = project.update(cx, |project, cx| {
246        project.fake_debug_session(
247            dap::DebugRequestType::Launch(LaunchConfig::default()),
248            None,
249            false,
250            cx,
251        )
252    });
253
254    let session = task.await.unwrap();
255    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
256
257    client
258        .on_request::<Threads, _>(move |_, _| {
259            Ok(dap::ThreadsResponse {
260                threads: vec![dap::Thread {
261                    id: 1,
262                    name: "Thread 1".into(),
263                }],
264            })
265        })
266        .await;
267
268    let stack_frames = vec![
269        StackFrame {
270            id: 1,
271            name: "Stack Frame 1".into(),
272            source: Some(dap::Source {
273                name: Some("test.js".into()),
274                path: Some(path!("/project/src/test.js").into()),
275                source_reference: None,
276                presentation_hint: None,
277                origin: None,
278                sources: None,
279                adapter_data: None,
280                checksums: None,
281            }),
282            line: 3,
283            column: 1,
284            end_line: None,
285            end_column: None,
286            can_restart: None,
287            instruction_pointer_reference: None,
288            module_id: None,
289            presentation_hint: None,
290        },
291        StackFrame {
292            id: 2,
293            name: "Stack Frame 2".into(),
294            source: Some(dap::Source {
295                name: Some("module.js".into()),
296                path: Some(path!("/project/src/module.js").into()),
297                source_reference: None,
298                presentation_hint: None,
299                origin: None,
300                sources: None,
301                adapter_data: None,
302                checksums: None,
303            }),
304            line: 1,
305            column: 1,
306            end_line: None,
307            end_column: None,
308            can_restart: None,
309            instruction_pointer_reference: None,
310            module_id: None,
311            presentation_hint: None,
312        },
313    ];
314
315    client
316        .on_request::<StackTrace, _>({
317            let stack_frames = Arc::new(stack_frames.clone());
318            move |_, args| {
319                assert_eq!(1, args.thread_id);
320
321                Ok(dap::StackTraceResponse {
322                    stack_frames: (*stack_frames).clone(),
323                    total_frames: None,
324                })
325            }
326        })
327        .await;
328
329    client
330        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
331            reason: dap::StoppedEventReason::Pause,
332            description: None,
333            thread_id: Some(1),
334            preserve_focus_hint: None,
335            text: None,
336            all_threads_stopped: None,
337            hit_breakpoint_ids: None,
338        }))
339        .await;
340
341    cx.run_until_parked();
342
343    // trigger threads to load
344    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
345        session
346            .mode()
347            .as_running()
348            .unwrap()
349            .update(cx, |running_state, cx| {
350                running_state
351                    .session()
352                    .update(cx, |session, cx| session.threads(cx));
353            });
354    });
355
356    cx.run_until_parked();
357
358    // select first thread
359    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
360        session
361            .mode()
362            .as_running()
363            .unwrap()
364            .update(cx, |running_state, cx| {
365                running_state.select_current_thread(
366                    &running_state
367                        .session()
368                        .update(cx, |session, cx| session.threads(cx)),
369                    cx,
370                );
371            });
372    });
373
374    cx.run_until_parked();
375
376    workspace
377        .update(cx, |workspace, window, cx| {
378            let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
379            assert_eq!(1, editors.len());
380
381            let project_path = editors[0]
382                .update(cx, |editor, cx| editor.project_path(cx))
383                .unwrap();
384            let expected = if cfg!(target_os = "windows") {
385                "src\\test.js"
386            } else {
387                "src/test.js"
388            };
389            assert_eq!(expected, project_path.path.to_string_lossy());
390            assert_eq!(test_file_content, editors[0].read(cx).text(cx));
391            assert_eq!(
392                vec![2..3],
393                editors[0].update(cx, |editor, cx| {
394                    let snapshot = editor.snapshot(window, cx);
395
396                    editor
397                        .highlighted_rows::<editor::DebugCurrentRowHighlight>()
398                        .map(|(range, _)| {
399                            let start = range.start.to_point(&snapshot.buffer_snapshot);
400                            let end = range.end.to_point(&snapshot.buffer_snapshot);
401                            start.row..end.row
402                        })
403                        .collect::<Vec<_>>()
404                })
405            );
406        })
407        .unwrap();
408
409    let stack_frame_list = workspace
410        .update(cx, |workspace, _window, cx| {
411            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
412            let active_debug_panel_item = debug_panel
413                .update(cx, |this, _| this.active_session())
414                .unwrap();
415
416            active_debug_panel_item
417                .read(cx)
418                .mode()
419                .as_running()
420                .unwrap()
421                .read(cx)
422                .stack_frame_list()
423                .clone()
424        })
425        .unwrap();
426
427    stack_frame_list.update(cx, |stack_frame_list, cx| {
428        assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
429        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
430    });
431
432    // select second stack frame
433    stack_frame_list
434        .update_in(cx, |stack_frame_list, window, cx| {
435            stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
436        })
437        .await
438        .unwrap();
439
440    cx.run_until_parked();
441
442    stack_frame_list.update(cx, |stack_frame_list, cx| {
443        assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
444        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
445    });
446
447    let _ = workspace.update(cx, |workspace, window, cx| {
448        let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
449        assert_eq!(1, editors.len());
450
451        let project_path = editors[0]
452            .update(cx, |editor, cx| editor.project_path(cx))
453            .unwrap();
454        let expected = if cfg!(target_os = "windows") {
455            "src\\module.js"
456        } else {
457            "src/module.js"
458        };
459        assert_eq!(expected, project_path.path.to_string_lossy());
460        assert_eq!(module_file_content, editors[0].read(cx).text(cx));
461        assert_eq!(
462            vec![0..1],
463            editors[0].update(cx, |editor, cx| {
464                let snapshot = editor.snapshot(window, cx);
465
466                editor
467                    .highlighted_rows::<editor::DebugCurrentRowHighlight>()
468                    .map(|(range, _)| {
469                        let start = range.start.to_point(&snapshot.buffer_snapshot);
470                        let end = range.end.to_point(&snapshot.buffer_snapshot);
471                        start.row..end.row
472                    })
473                    .collect::<Vec<_>>()
474            })
475        );
476    });
477
478    let shutdown_session = project.update(cx, |project, cx| {
479        project.dap_store().update(cx, |dap_store, cx| {
480            dap_store.shutdown_session(session.read(cx).session_id(), cx)
481        })
482    });
483
484    shutdown_session.await.unwrap();
485}
486
487#[gpui::test]
488async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
489    init_test(cx);
490
491    let fs = FakeFs::new(executor.clone());
492
493    let test_file_content = r#"
494        import { SOME_VALUE } './module.js';
495
496        console.log(SOME_VALUE);
497    "#
498    .unindent();
499
500    let module_file_content = r#"
501        export SOME_VALUE = 'some value';
502    "#
503    .unindent();
504
505    fs.insert_tree(
506        path!("/project"),
507        json!({
508           "src": {
509               "test.js": test_file_content,
510               "module.js": module_file_content,
511           }
512        }),
513    )
514    .await;
515
516    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
517    let workspace = init_test_workspace(&project, cx).await;
518    let cx = &mut VisualTestContext::from_window(*workspace, cx);
519
520    let task = project.update(cx, |project, cx| {
521        project.fake_debug_session(
522            dap::DebugRequestType::Launch(LaunchConfig::default()),
523            None,
524            false,
525            cx,
526        )
527    });
528
529    let session = task.await.unwrap();
530    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
531
532    client
533        .on_request::<Threads, _>(move |_, _| {
534            Ok(dap::ThreadsResponse {
535                threads: vec![dap::Thread {
536                    id: 1,
537                    name: "Thread 1".into(),
538                }],
539            })
540        })
541        .await;
542
543    let stack_frames = vec![
544        StackFrame {
545            id: 1,
546            name: "Stack Frame 1".into(),
547            source: Some(dap::Source {
548                name: Some("test.js".into()),
549                path: Some(path!("/project/src/test.js").into()),
550                source_reference: None,
551                presentation_hint: None,
552                origin: None,
553                sources: None,
554                adapter_data: None,
555                checksums: None,
556            }),
557            line: 3,
558            column: 1,
559            end_line: None,
560            end_column: None,
561            can_restart: None,
562            instruction_pointer_reference: None,
563            module_id: None,
564            presentation_hint: None,
565        },
566        StackFrame {
567            id: 2,
568            name: "Stack Frame 2".into(),
569            source: Some(dap::Source {
570                name: Some("module.js".into()),
571                path: Some(path!("/project/src/module.js").into()),
572                source_reference: None,
573                presentation_hint: None,
574                origin: Some("ignored".into()),
575                sources: None,
576                adapter_data: None,
577                checksums: None,
578            }),
579            line: 1,
580            column: 1,
581            end_line: None,
582            end_column: None,
583            can_restart: None,
584            instruction_pointer_reference: None,
585            module_id: None,
586            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
587        },
588        StackFrame {
589            id: 3,
590            name: "Stack Frame 3".into(),
591            source: Some(dap::Source {
592                name: Some("module.js".into()),
593                path: Some(path!("/project/src/module.js").into()),
594                source_reference: None,
595                presentation_hint: None,
596                origin: Some("ignored".into()),
597                sources: None,
598                adapter_data: None,
599                checksums: None,
600            }),
601            line: 1,
602            column: 1,
603            end_line: None,
604            end_column: None,
605            can_restart: None,
606            instruction_pointer_reference: None,
607            module_id: None,
608            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
609        },
610        StackFrame {
611            id: 4,
612            name: "Stack Frame 4".into(),
613            source: Some(dap::Source {
614                name: Some("module.js".into()),
615                path: Some(path!("/project/src/module.js").into()),
616                source_reference: None,
617                presentation_hint: None,
618                origin: None,
619                sources: None,
620                adapter_data: None,
621                checksums: None,
622            }),
623            line: 1,
624            column: 1,
625            end_line: None,
626            end_column: None,
627            can_restart: None,
628            instruction_pointer_reference: None,
629            module_id: None,
630            presentation_hint: None,
631        },
632        StackFrame {
633            id: 5,
634            name: "Stack Frame 5".into(),
635            source: Some(dap::Source {
636                name: Some("module.js".into()),
637                path: Some(path!("/project/src/module.js").into()),
638                source_reference: None,
639                presentation_hint: None,
640                origin: None,
641                sources: None,
642                adapter_data: None,
643                checksums: None,
644            }),
645            line: 1,
646            column: 1,
647            end_line: None,
648            end_column: None,
649            can_restart: None,
650            instruction_pointer_reference: None,
651            module_id: None,
652            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
653        },
654        StackFrame {
655            id: 6,
656            name: "Stack Frame 6".into(),
657            source: Some(dap::Source {
658                name: Some("module.js".into()),
659                path: Some(path!("/project/src/module.js").into()),
660                source_reference: None,
661                presentation_hint: None,
662                origin: None,
663                sources: None,
664                adapter_data: None,
665                checksums: None,
666            }),
667            line: 1,
668            column: 1,
669            end_line: None,
670            end_column: None,
671            can_restart: None,
672            instruction_pointer_reference: None,
673            module_id: None,
674            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
675        },
676        StackFrame {
677            id: 7,
678            name: "Stack Frame 7".into(),
679            source: Some(dap::Source {
680                name: Some("module.js".into()),
681                path: Some(path!("/project/src/module.js").into()),
682                source_reference: None,
683                presentation_hint: None,
684                origin: None,
685                sources: None,
686                adapter_data: None,
687                checksums: None,
688            }),
689            line: 1,
690            column: 1,
691            end_line: None,
692            end_column: None,
693            can_restart: None,
694            instruction_pointer_reference: None,
695            module_id: None,
696            presentation_hint: None,
697        },
698    ];
699
700    client
701        .on_request::<StackTrace, _>({
702            let stack_frames = Arc::new(stack_frames.clone());
703            move |_, args| {
704                assert_eq!(1, args.thread_id);
705
706                Ok(dap::StackTraceResponse {
707                    stack_frames: (*stack_frames).clone(),
708                    total_frames: None,
709                })
710            }
711        })
712        .await;
713
714    client
715        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
716            reason: dap::StoppedEventReason::Pause,
717            description: None,
718            thread_id: Some(1),
719            preserve_focus_hint: None,
720            text: None,
721            all_threads_stopped: None,
722            hit_breakpoint_ids: None,
723        }))
724        .await;
725
726    cx.run_until_parked();
727
728    // trigger threads to load
729    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
730        session
731            .mode()
732            .as_running()
733            .unwrap()
734            .update(cx, |running_state, cx| {
735                running_state
736                    .session()
737                    .update(cx, |session, cx| session.threads(cx));
738            });
739    });
740
741    cx.run_until_parked();
742
743    // select first thread
744    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
745        session
746            .mode()
747            .as_running()
748            .unwrap()
749            .update(cx, |running_state, cx| {
750                running_state.select_current_thread(
751                    &running_state
752                        .session()
753                        .update(cx, |session, cx| session.threads(cx)),
754                    cx,
755                );
756            });
757    });
758
759    cx.run_until_parked();
760
761    // trigger stack frames to loaded
762    active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
763        let stack_frame_list = debug_panel_item
764            .mode()
765            .as_running()
766            .unwrap()
767            .update(cx, |state, _| state.stack_frame_list().clone());
768
769        stack_frame_list.update(cx, |stack_frame_list, cx| {
770            stack_frame_list.dap_stack_frames(cx);
771        });
772    });
773
774    cx.run_until_parked();
775
776    active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
777        let stack_frame_list = debug_panel_item
778            .mode()
779            .as_running()
780            .unwrap()
781            .update(cx, |state, _| state.stack_frame_list().clone());
782
783        stack_frame_list.update(cx, |stack_frame_list, cx| {
784            stack_frame_list.build_entries(true, window, cx);
785
786            assert_eq!(
787                &vec![
788                    StackFrameEntry::Normal(stack_frames[0].clone()),
789                    StackFrameEntry::Collapsed(vec![
790                        stack_frames[1].clone(),
791                        stack_frames[2].clone()
792                    ]),
793                    StackFrameEntry::Normal(stack_frames[3].clone()),
794                    StackFrameEntry::Collapsed(vec![
795                        stack_frames[4].clone(),
796                        stack_frames[5].clone()
797                    ]),
798                    StackFrameEntry::Normal(stack_frames[6].clone()),
799                ],
800                stack_frame_list.entries()
801            );
802
803            stack_frame_list.expand_collapsed_entry(
804                1,
805                &vec![stack_frames[1].clone(), stack_frames[2].clone()],
806                cx,
807            );
808
809            assert_eq!(
810                &vec![
811                    StackFrameEntry::Normal(stack_frames[0].clone()),
812                    StackFrameEntry::Normal(stack_frames[1].clone()),
813                    StackFrameEntry::Normal(stack_frames[2].clone()),
814                    StackFrameEntry::Normal(stack_frames[3].clone()),
815                    StackFrameEntry::Collapsed(vec![
816                        stack_frames[4].clone(),
817                        stack_frames[5].clone()
818                    ]),
819                    StackFrameEntry::Normal(stack_frames[6].clone()),
820                ],
821                stack_frame_list.entries()
822            );
823
824            stack_frame_list.expand_collapsed_entry(
825                4,
826                &vec![stack_frames[4].clone(), stack_frames[5].clone()],
827                cx,
828            );
829
830            assert_eq!(
831                &vec![
832                    StackFrameEntry::Normal(stack_frames[0].clone()),
833                    StackFrameEntry::Normal(stack_frames[1].clone()),
834                    StackFrameEntry::Normal(stack_frames[2].clone()),
835                    StackFrameEntry::Normal(stack_frames[3].clone()),
836                    StackFrameEntry::Normal(stack_frames[4].clone()),
837                    StackFrameEntry::Normal(stack_frames[5].clone()),
838                    StackFrameEntry::Normal(stack_frames[6].clone()),
839                ],
840                stack_frame_list.entries()
841            );
842        });
843    });
844
845    let shutdown_session = project.update(cx, |project, cx| {
846        project.dap_store().update(cx, |dap_store, cx| {
847            dap_store.shutdown_session(session.read(cx).session_id(), cx)
848        })
849    });
850
851    shutdown_session.await.unwrap();
852}