1use super::{
2 stack_frame_list::{StackFrameList, StackFrameListEvent},
3 variable_list::VariableList,
4};
5use alacritty_terminal::vte::ansi;
6use anyhow::Result;
7use collections::HashMap;
8use dap::OutputEvent;
9use editor::{
10 BackgroundHighlight, Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId,
11};
12use fuzzy::StringMatchCandidate;
13use gpui::{
14 Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
15 TextStyle, WeakEntity,
16};
17use language::{Buffer, CodeLabel, ToOffset};
18use menu::Confirm;
19use project::{
20 Completion, CompletionResponse,
21 debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
22};
23use settings::Settings;
24use std::{cell::RefCell, ops::Range, rc::Rc, usize};
25use theme::{Theme, ThemeSettings};
26use ui::{Divider, prelude::*};
27
28pub struct Console {
29 console: Entity<Editor>,
30 query_bar: Entity<Editor>,
31 session: Entity<Session>,
32 _subscriptions: Vec<Subscription>,
33 variable_list: Entity<VariableList>,
34 stack_frame_list: Entity<StackFrameList>,
35 last_token: OutputToken,
36 update_output_task: Task<()>,
37 ansi_handler: ConsoleHandler,
38 ansi_processor: ansi::Processor<ansi::StdSyncHandler>,
39 focus_handle: FocusHandle,
40}
41
42impl Console {
43 pub fn new(
44 session: Entity<Session>,
45 stack_frame_list: Entity<StackFrameList>,
46 variable_list: Entity<VariableList>,
47 window: &mut Window,
48 cx: &mut Context<Self>,
49 ) -> Self {
50 let console = cx.new(|cx| {
51 let mut editor = Editor::multi_line(window, cx);
52 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
53 editor.set_read_only(true);
54 editor.disable_scrollbars_and_minimap(window, cx);
55 editor.set_show_gutter(false, cx);
56 editor.set_show_runnables(false, cx);
57 editor.set_show_breakpoints(false, cx);
58 editor.set_show_code_actions(false, cx);
59 editor.set_show_line_numbers(false, cx);
60 editor.set_show_git_diff_gutter(false, cx);
61 editor.set_autoindent(false);
62 editor.set_input_enabled(false);
63 editor.set_use_autoclose(false);
64 editor.set_show_wrap_guides(false, cx);
65 editor.set_show_indent_guides(false, cx);
66 editor.set_show_edit_predictions(Some(false), window, cx);
67 editor.set_use_modal_editing(false);
68 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
69 editor
70 });
71 let focus_handle = cx.focus_handle();
72
73 let this = cx.weak_entity();
74 let query_bar = cx.new(|cx| {
75 let mut editor = Editor::single_line(window, cx);
76 editor.set_placeholder_text("Evaluate an expression", cx);
77 editor.set_use_autoclose(false);
78 editor.set_show_gutter(false, cx);
79 editor.set_show_wrap_guides(false, cx);
80 editor.set_show_indent_guides(false, cx);
81 editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
82
83 editor
84 });
85
86 let _subscriptions = vec![
87 cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
88 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
89 if let SessionEvent::ConsoleOutput = event {
90 this.update_output(window, cx)
91 }
92 }),
93 cx.on_focus(&focus_handle, window, |console, window, cx| {
94 if console.is_running(cx) {
95 console.query_bar.focus_handle(cx).focus(window);
96 }
97 }),
98 ];
99
100 Self {
101 session,
102 console,
103 query_bar,
104 variable_list,
105 _subscriptions,
106 stack_frame_list,
107 update_output_task: Task::ready(()),
108 last_token: OutputToken(0),
109 ansi_handler: Default::default(),
110 ansi_processor: Default::default(),
111 focus_handle,
112 }
113 }
114
115 #[cfg(test)]
116 pub(crate) fn editor(&self) -> &Entity<Editor> {
117 &self.console
118 }
119
120 fn is_running(&self, cx: &Context<Self>) -> bool {
121 self.session.read(cx).is_running()
122 }
123
124 fn handle_stack_frame_list_events(
125 &mut self,
126 _: Entity<StackFrameList>,
127 event: &StackFrameListEvent,
128 cx: &mut Context<Self>,
129 ) {
130 match event {
131 StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
132 StackFrameListEvent::BuiltEntries => {}
133 }
134 }
135
136 pub(crate) fn show_indicator(&self, cx: &App) -> bool {
137 self.session.read(cx).has_new_output(self.last_token)
138 }
139
140 pub fn add_messages<'a>(
141 &mut self,
142 events: impl Iterator<Item = &'a OutputEvent>,
143 window: &mut Window,
144 cx: &mut App,
145 ) {
146 let mut to_insert = String::default();
147 for event in events {
148 use std::fmt::Write;
149
150 _ = write!(to_insert, "{}\n", event.output.trim_end());
151 }
152
153 let len = self.ansi_handler.pos;
154 self.ansi_processor
155 .advance(&mut self.ansi_handler, to_insert.as_bytes());
156 let output = std::mem::take(&mut self.ansi_handler.output);
157 let mut spans = std::mem::take(&mut self.ansi_handler.spans);
158 let mut background_spans = std::mem::take(&mut self.ansi_handler.background_spans);
159 if self.ansi_handler.current_range_start < len + output.len() {
160 spans.push((
161 self.ansi_handler.current_range_start..len + output.len(),
162 self.ansi_handler.current_color,
163 ));
164 self.ansi_handler.current_range_start = len + output.len();
165 }
166 if self.ansi_handler.current_background_range_start < len + output.len() {
167 background_spans.push((
168 self.ansi_handler.current_background_range_start..len + output.len(),
169 self.ansi_handler.current_background_color,
170 ));
171 self.ansi_handler.current_background_range_start = len + output.len();
172 }
173
174 self.console.update(cx, |console, cx| {
175 struct ConsoleAnsiHighlight;
176
177 console.set_read_only(false);
178 console.move_to_end(&editor::actions::MoveToEnd, window, cx);
179 console.insert(&output, window, cx);
180 let buffer = console.buffer().read(cx).snapshot(cx);
181
182 let mut highlights = console
183 .remove_text_highlights::<ConsoleAnsiHighlight>(cx)
184 .unwrap_or_default();
185 for (range, color) in spans {
186 let Some(color) = color else { continue };
187 let start = range.start + len;
188 let range = start..range.end + len;
189 let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
190 let style = HighlightStyle {
191 color: Some(terminal_view::terminal_element::convert_color(
192 &color,
193 cx.theme(),
194 )),
195 ..Default::default()
196 };
197 highlights.push((range, style));
198 }
199 console.highlight_text::<ConsoleAnsiHighlight>(highlights, cx);
200
201 let mut background_highlights = console
202 .clear_background_highlights::<ConsoleAnsiHighlight>(cx)
203 .unwrap_or_default();
204 for (range, color) in background_spans {
205 let Some(color) = color else { continue };
206 let start = range.start + len;
207 let range = start..range.end + len;
208 let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
209
210 let color_fetcher: fn(&Theme) -> Hsla = match color {
211 // Named and theme defined colors
212 ansi::Color::Named(n) => match n {
213 ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
214 ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
215 ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
216 ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
217 ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
218 ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta,
219 ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
220 ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
221 ansi::NamedColor::BrightBlack => {
222 |theme| theme.colors().terminal_ansi_bright_black
223 }
224 ansi::NamedColor::BrightRed => {
225 |theme| theme.colors().terminal_ansi_bright_red
226 }
227 ansi::NamedColor::BrightGreen => {
228 |theme| theme.colors().terminal_ansi_bright_green
229 }
230 ansi::NamedColor::BrightYellow => {
231 |theme| theme.colors().terminal_ansi_bright_yellow
232 }
233 ansi::NamedColor::BrightBlue => {
234 |theme| theme.colors().terminal_ansi_bright_blue
235 }
236 ansi::NamedColor::BrightMagenta => {
237 |theme| theme.colors().terminal_ansi_bright_magenta
238 }
239 ansi::NamedColor::BrightCyan => {
240 |theme| theme.colors().terminal_ansi_bright_cyan
241 }
242 ansi::NamedColor::BrightWhite => {
243 |theme| theme.colors().terminal_ansi_bright_white
244 }
245 ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground,
246 ansi::NamedColor::Background => |theme| theme.colors().terminal_background,
247 ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
248 ansi::NamedColor::DimBlack => {
249 |theme| theme.colors().terminal_ansi_dim_black
250 }
251 ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red,
252 ansi::NamedColor::DimGreen => {
253 |theme| theme.colors().terminal_ansi_dim_green
254 }
255 ansi::NamedColor::DimYellow => {
256 |theme| theme.colors().terminal_ansi_dim_yellow
257 }
258 ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue,
259 ansi::NamedColor::DimMagenta => {
260 |theme| theme.colors().terminal_ansi_dim_magenta
261 }
262 ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan,
263 ansi::NamedColor::DimWhite => {
264 |theme| theme.colors().terminal_ansi_dim_white
265 }
266 ansi::NamedColor::BrightForeground => {
267 |theme| theme.colors().terminal_bright_foreground
268 }
269 ansi::NamedColor::DimForeground => {
270 |theme| theme.colors().terminal_dim_foreground
271 }
272 },
273 // 'True' colors
274 ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
275 // 8 bit, indexed colors
276 ansi::Color::Indexed(i) => {
277 match i {
278 // 0-15 are the same as the named colors above
279 0 => |theme| theme.colors().terminal_ansi_black,
280 1 => |theme| theme.colors().terminal_ansi_red,
281 2 => |theme| theme.colors().terminal_ansi_green,
282 3 => |theme| theme.colors().terminal_ansi_yellow,
283 4 => |theme| theme.colors().terminal_ansi_blue,
284 5 => |theme| theme.colors().terminal_ansi_magenta,
285 6 => |theme| theme.colors().terminal_ansi_cyan,
286 7 => |theme| theme.colors().terminal_ansi_white,
287 8 => |theme| theme.colors().terminal_ansi_bright_black,
288 9 => |theme| theme.colors().terminal_ansi_bright_red,
289 10 => |theme| theme.colors().terminal_ansi_bright_green,
290 11 => |theme| theme.colors().terminal_ansi_bright_yellow,
291 12 => |theme| theme.colors().terminal_ansi_bright_blue,
292 13 => |theme| theme.colors().terminal_ansi_bright_magenta,
293 14 => |theme| theme.colors().terminal_ansi_bright_cyan,
294 15 => |theme| theme.colors().terminal_ansi_bright_white,
295 // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
296 // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
297 // 16..=231 => {
298 // let (r, g, b) = rgb_for_index(index as u8);
299 // rgba_color(
300 // if r == 0 { 0 } else { r * 40 + 55 },
301 // if g == 0 { 0 } else { g * 40 + 55 },
302 // if b == 0 { 0 } else { b * 40 + 55 },
303 // )
304 // }
305 // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
306 // 232..=255 => {
307 // let i = index as u8 - 232; // Align index to 0..24
308 // let value = i * 10 + 8;
309 // rgba_color(value, value, value)
310 // }
311 // For compatibility with the alacritty::Colors interface
312 // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
313 _ => |_| gpui::black(),
314 }
315 }
316 };
317
318 background_highlights.push(BackgroundHighlight {
319 range,
320 color_fetcher,
321 });
322 }
323 console.highlight_background_ranges::<ConsoleAnsiHighlight>(background_highlights, cx);
324
325 console.set_read_only(true);
326
327 cx.notify();
328 });
329 }
330
331 pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
332 let expression = self.query_bar.update(cx, |editor, cx| {
333 let expression = editor.text(cx);
334 cx.defer_in(window, |editor, window, cx| {
335 editor.clear(window, cx);
336 });
337
338 expression
339 });
340
341 self.session.update(cx, |session, cx| {
342 session
343 .evaluate(
344 expression,
345 Some(dap::EvaluateArgumentsContext::Repl),
346 self.stack_frame_list.read(cx).opened_stack_frame_id(),
347 None,
348 cx,
349 )
350 .detach();
351 });
352 }
353
354 fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
355 EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
356 }
357
358 fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
359 let is_read_only = editor.read(cx).read_only(cx);
360 let settings = ThemeSettings::get_global(cx);
361 let theme = cx.theme();
362 let text_style = TextStyle {
363 color: if is_read_only {
364 theme.colors().text_muted
365 } else {
366 theme.colors().text
367 },
368 font_family: settings.buffer_font.family.clone(),
369 font_features: settings.buffer_font.features.clone(),
370 font_size: settings.buffer_font_size(cx).into(),
371 font_weight: settings.buffer_font.weight,
372 line_height: relative(settings.buffer_line_height.value()),
373 ..Default::default()
374 };
375 EditorStyle {
376 background: theme.colors().editor_background,
377 local_player: theme.players().local(),
378 text: text_style,
379 ..Default::default()
380 }
381 }
382
383 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
384 EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
385 }
386
387 fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
388 let session = self.session.clone();
389 let token = self.last_token;
390
391 self.update_output_task = cx.spawn_in(window, async move |this, cx| {
392 _ = session.update_in(cx, move |session, window, cx| {
393 let (output, last_processed_token) = session.output(token);
394
395 _ = this.update(cx, |this, cx| {
396 if last_processed_token == this.last_token {
397 return;
398 }
399 this.add_messages(output, window, cx);
400
401 this.last_token = last_processed_token;
402 });
403 });
404 });
405 }
406}
407
408impl Render for Console {
409 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
410 v_flex()
411 .track_focus(&self.focus_handle)
412 .key_context("DebugConsole")
413 .on_action(cx.listener(Self::evaluate))
414 .size_full()
415 .child(self.render_console(cx))
416 .when(self.is_running(cx), |this| {
417 this.child(Divider::horizontal())
418 .child(self.render_query_bar(cx))
419 })
420 .border_2()
421 }
422}
423
424impl Focusable for Console {
425 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
426 self.focus_handle.clone()
427 }
428}
429
430struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
431
432impl CompletionProvider for ConsoleQueryBarCompletionProvider {
433 fn completions(
434 &self,
435 _excerpt_id: ExcerptId,
436 buffer: &Entity<Buffer>,
437 buffer_position: language::Anchor,
438 _trigger: editor::CompletionContext,
439 _window: &mut Window,
440 cx: &mut Context<Editor>,
441 ) -> Task<Result<Vec<CompletionResponse>>> {
442 let Some(console) = self.0.upgrade() else {
443 return Task::ready(Ok(Vec::new()));
444 };
445
446 let support_completions = console
447 .read(cx)
448 .session
449 .read(cx)
450 .capabilities()
451 .supports_completions_request
452 .unwrap_or_default();
453
454 if support_completions {
455 self.client_completions(&console, buffer, buffer_position, cx)
456 } else {
457 self.variable_list_completions(&console, buffer, buffer_position, cx)
458 }
459 }
460
461 fn apply_additional_edits_for_completion(
462 &self,
463 _buffer: Entity<Buffer>,
464 _completions: Rc<RefCell<Box<[Completion]>>>,
465 _completion_index: usize,
466 _push_to_history: bool,
467 _cx: &mut Context<Editor>,
468 ) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
469 Task::ready(Ok(None))
470 }
471
472 fn is_completion_trigger(
473 &self,
474 _buffer: &Entity<Buffer>,
475 _position: language::Anchor,
476 _text: &str,
477 _trigger_in_words: bool,
478 _menu_is_open: bool,
479 _cx: &mut Context<Editor>,
480 ) -> bool {
481 true
482 }
483}
484
485impl ConsoleQueryBarCompletionProvider {
486 fn variable_list_completions(
487 &self,
488 console: &Entity<Console>,
489 buffer: &Entity<Buffer>,
490 buffer_position: language::Anchor,
491 cx: &mut Context<Editor>,
492 ) -> Task<Result<Vec<CompletionResponse>>> {
493 let (variables, string_matches) = console.update(cx, |console, cx| {
494 let mut variables = HashMap::default();
495 let mut string_matches = Vec::default();
496
497 for variable in console.variable_list.update(cx, |variable_list, cx| {
498 variable_list.completion_variables(cx)
499 }) {
500 if let Some(evaluate_name) = &variable.evaluate_name {
501 variables.insert(evaluate_name.clone(), variable.value.clone());
502 string_matches.push(StringMatchCandidate {
503 id: 0,
504 string: evaluate_name.clone(),
505 char_bag: evaluate_name.chars().collect(),
506 });
507 }
508
509 variables.insert(variable.name.clone(), variable.value.clone());
510
511 string_matches.push(StringMatchCandidate {
512 id: 0,
513 string: variable.name.clone(),
514 char_bag: variable.name.chars().collect(),
515 });
516 }
517
518 (variables, string_matches)
519 });
520
521 let query = buffer.read(cx).text();
522
523 cx.spawn(async move |_, cx| {
524 const LIMIT: usize = 10;
525 let matches = fuzzy::match_strings(
526 &string_matches,
527 &query,
528 true,
529 LIMIT,
530 &Default::default(),
531 cx.background_executor().clone(),
532 )
533 .await;
534
535 let completions = matches
536 .iter()
537 .filter_map(|string_match| {
538 let variable_value = variables.get(&string_match.string)?;
539
540 Some(project::Completion {
541 replace_range: buffer_position..buffer_position,
542 new_text: string_match.string.clone(),
543 label: CodeLabel {
544 filter_range: 0..string_match.string.len(),
545 text: format!("{} {}", string_match.string, variable_value),
546 runs: Vec::new(),
547 },
548 icon_path: None,
549 documentation: None,
550 confirm: None,
551 source: project::CompletionSource::Custom,
552 insert_text_mode: None,
553 })
554 })
555 .collect::<Vec<_>>();
556
557 Ok(vec![project::CompletionResponse {
558 is_incomplete: completions.len() >= LIMIT,
559 completions,
560 }])
561 })
562 }
563
564 fn client_completions(
565 &self,
566 console: &Entity<Console>,
567 buffer: &Entity<Buffer>,
568 buffer_position: language::Anchor,
569 cx: &mut Context<Editor>,
570 ) -> Task<Result<Vec<CompletionResponse>>> {
571 let completion_task = console.update(cx, |console, cx| {
572 console.session.update(cx, |state, cx| {
573 let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
574
575 state.completions(
576 CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
577 cx,
578 )
579 })
580 });
581 let snapshot = buffer.read(cx).text_snapshot();
582 cx.background_executor().spawn(async move {
583 let completions = completion_task.await?;
584
585 let completions = completions
586 .into_iter()
587 .map(|completion| {
588 let new_text = completion
589 .text
590 .as_ref()
591 .unwrap_or(&completion.label)
592 .to_owned();
593 let buffer_text = snapshot.text();
594 let buffer_bytes = buffer_text.as_bytes();
595 let new_bytes = new_text.as_bytes();
596
597 let mut prefix_len = 0;
598 for i in (0..new_bytes.len()).rev() {
599 if buffer_bytes.ends_with(&new_bytes[0..i]) {
600 prefix_len = i;
601 break;
602 }
603 }
604
605 let buffer_offset = buffer_position.to_offset(&snapshot);
606 let start = buffer_offset - prefix_len;
607 let start = snapshot.clip_offset(start, Bias::Left);
608 let start = snapshot.anchor_before(start);
609 let replace_range = start..buffer_position;
610
611 project::Completion {
612 replace_range,
613 new_text,
614 label: CodeLabel {
615 filter_range: 0..completion.label.len(),
616 text: completion.label,
617 runs: Vec::new(),
618 },
619 icon_path: None,
620 documentation: None,
621 confirm: None,
622 source: project::CompletionSource::BufferWord {
623 word_range: buffer_position..language::Anchor::MAX,
624 resolved: false,
625 },
626 insert_text_mode: None,
627 }
628 })
629 .collect();
630
631 Ok(vec![project::CompletionResponse {
632 completions,
633 is_incomplete: false,
634 }])
635 })
636 }
637}
638
639#[derive(Default)]
640struct ConsoleHandler {
641 output: String,
642 spans: Vec<(Range<usize>, Option<ansi::Color>)>,
643 background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
644 current_range_start: usize,
645 current_background_range_start: usize,
646 current_color: Option<ansi::Color>,
647 current_background_color: Option<ansi::Color>,
648 pos: usize,
649}
650
651impl ConsoleHandler {
652 fn break_span(&mut self, color: Option<ansi::Color>) {
653 self.spans.push((
654 self.current_range_start..self.output.len(),
655 self.current_color,
656 ));
657 self.current_color = color;
658 self.current_range_start = self.pos;
659 }
660
661 fn break_background_span(&mut self, color: Option<ansi::Color>) {
662 self.background_spans.push((
663 self.current_background_range_start..self.output.len(),
664 self.current_background_color,
665 ));
666 self.current_background_color = color;
667 self.current_background_range_start = self.pos;
668 }
669}
670
671impl ansi::Handler for ConsoleHandler {
672 fn input(&mut self, c: char) {
673 self.output.push(c);
674 self.pos += 1;
675 }
676
677 fn linefeed(&mut self) {
678 self.output.push('\n');
679 self.pos += 1;
680 }
681
682 fn put_tab(&mut self, count: u16) {
683 self.output
684 .extend(std::iter::repeat('\t').take(count as usize));
685 self.pos += count as usize;
686 }
687
688 fn terminal_attribute(&mut self, attr: ansi::Attr) {
689 match attr {
690 ansi::Attr::Foreground(color) => {
691 self.break_span(Some(color));
692 }
693 ansi::Attr::Background(color) => {
694 self.break_background_span(Some(color));
695 }
696 ansi::Attr::Reset => {
697 self.break_span(None);
698 self.break_background_span(None);
699 }
700 _ => {}
701 }
702 }
703}