practicode 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +28 -8
- package/SECURITY.md +11 -0
- package/assets/i18n/en.json +13 -4
- package/assets/i18n/es.json +13 -4
- package/assets/i18n/ja.json +13 -4
- package/assets/i18n/ko.json +13 -4
- package/assets/i18n/zh.json +13 -4
- package/docs/ARCHITECTURE.md +10 -5
- package/docs/MAINTAINING.md +5 -0
- package/package.json +7 -2
- package/src/ai.rs +103 -4
- package/src/core.rs +63 -4
- package/src/lib.rs +2 -6
- package/src/tui/commands.rs +14 -0
- package/src/tui/editor.rs +180 -0
- package/src/tui/problem_view.rs +138 -0
- package/src/tui/settings_panel.rs +319 -0
- package/src/tui.rs +423 -398
package/src/core.rs
CHANGED
|
@@ -36,6 +36,10 @@ pub struct Settings {
|
|
|
36
36
|
pub topics: Vec<String>,
|
|
37
37
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
38
38
|
pub avoid_topics: Vec<String>,
|
|
39
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
40
|
+
pub generate_languages: Vec<String>,
|
|
41
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
42
|
+
pub generate_ui_languages: Vec<String>,
|
|
39
43
|
#[serde(default = "default_editor")]
|
|
40
44
|
pub editor: String,
|
|
41
45
|
#[serde(default = "default_next_source")]
|
|
@@ -57,6 +61,8 @@ impl Default for Settings {
|
|
|
57
61
|
difficulty: default_difficulty(),
|
|
58
62
|
topics: Vec::new(),
|
|
59
63
|
avoid_topics: Vec::new(),
|
|
64
|
+
generate_languages: Vec::new(),
|
|
65
|
+
generate_ui_languages: Vec::new(),
|
|
60
66
|
editor: default_editor(),
|
|
61
67
|
next_source: default_next_source(),
|
|
62
68
|
ai_provider: default_ai_provider(),
|
|
@@ -291,12 +297,19 @@ fn validate_bank(bank: &[Problem], path: &Path) -> Result<()> {
|
|
|
291
297
|
problem.id
|
|
292
298
|
);
|
|
293
299
|
}
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
if problem.answers.is_empty() {
|
|
301
|
+
bail!(
|
|
302
|
+
"{} problem {} must contain at least one answer",
|
|
303
|
+
path.display(),
|
|
304
|
+
problem.id
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
for language in problem.answers.keys() {
|
|
308
|
+
if !LANGUAGES.contains(&language.as_str()) {
|
|
296
309
|
bail!(
|
|
297
|
-
"{} problem {}
|
|
310
|
+
"{} problem {} has unsupported answer language {language}",
|
|
298
311
|
path.display(),
|
|
299
|
-
problem.id
|
|
312
|
+
problem.id,
|
|
300
313
|
);
|
|
301
314
|
}
|
|
302
315
|
}
|
|
@@ -381,6 +394,8 @@ pub fn normalize_settings(settings: &mut Settings) {
|
|
|
381
394
|
settings.difficulty = normalize_difficulty(&settings.difficulty);
|
|
382
395
|
settings.topics = normalize_topic_list(&settings.topics);
|
|
383
396
|
settings.avoid_topics = normalize_topic_list(&settings.avoid_topics);
|
|
397
|
+
settings.generate_languages = normalize_language_list(&settings.generate_languages);
|
|
398
|
+
settings.generate_ui_languages = normalize_ui_language_list(&settings.generate_ui_languages);
|
|
384
399
|
settings.next_source = normalize_next_source(&settings.next_source);
|
|
385
400
|
settings.ai_provider = normalize_ai_provider(&settings.ai_provider);
|
|
386
401
|
if settings.ai_model.trim().is_empty() {
|
|
@@ -401,6 +416,50 @@ pub fn normalize_language(language: &str) -> String {
|
|
|
401
416
|
}
|
|
402
417
|
}
|
|
403
418
|
|
|
419
|
+
pub fn parse_language_list(value: &str) -> Vec<String> {
|
|
420
|
+
let mut languages = Vec::new();
|
|
421
|
+
for language in value.split(',') {
|
|
422
|
+
let language = language.trim().to_lowercase();
|
|
423
|
+
if language == "all" {
|
|
424
|
+
return Vec::new();
|
|
425
|
+
}
|
|
426
|
+
if LANGUAGES.contains(&language.as_str()) && !languages.contains(&language) {
|
|
427
|
+
languages.push(language);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
languages
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
pub fn normalize_language_list(languages: &[String]) -> Vec<String> {
|
|
434
|
+
parse_language_list(&languages.join(","))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
pub fn parse_ui_language_list(value: &str) -> Vec<String> {
|
|
438
|
+
let mut languages = Vec::new();
|
|
439
|
+
for language in value.split(',') {
|
|
440
|
+
let lower = language.trim().to_lowercase();
|
|
441
|
+
if lower == "all" {
|
|
442
|
+
return Vec::new();
|
|
443
|
+
}
|
|
444
|
+
let language = lower
|
|
445
|
+
.split(['-', '_'])
|
|
446
|
+
.next()
|
|
447
|
+
.filter(|value| UI_LANGUAGES.contains(value))
|
|
448
|
+
.unwrap_or("");
|
|
449
|
+
if language == "all" {
|
|
450
|
+
return Vec::new();
|
|
451
|
+
}
|
|
452
|
+
if !language.is_empty() && !languages.iter().any(|value| value == language) {
|
|
453
|
+
languages.push(language.to_string());
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
languages
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
pub fn normalize_ui_language_list(languages: &[String]) -> Vec<String> {
|
|
460
|
+
parse_ui_language_list(&languages.join(","))
|
|
461
|
+
}
|
|
462
|
+
|
|
404
463
|
pub fn normalize_next_source(source: &str) -> String {
|
|
405
464
|
if source == "ai" {
|
|
406
465
|
"ai".to_string()
|
package/src/lib.rs
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
use anyhow::{Context, Result};
|
|
2
|
-
use crossterm::{
|
|
3
|
-
cursor::SetCursorStyle,
|
|
4
|
-
event::{DisableMouseCapture, EnableMouseCapture},
|
|
5
|
-
execute,
|
|
6
|
-
};
|
|
2
|
+
use crossterm::{cursor::SetCursorStyle, event::DisableMouseCapture, execute};
|
|
7
3
|
use std::{env, io::stdout};
|
|
8
4
|
|
|
9
5
|
pub mod ai;
|
|
@@ -59,7 +55,7 @@ pub fn run_cli() -> Result<()> {
|
|
|
59
55
|
|
|
60
56
|
let mut app = tui::PracticodeApp::new(root)?;
|
|
61
57
|
let mut terminal = ratatui::init();
|
|
62
|
-
let _ = execute!(stdout(), SetCursorStyle::SteadyBar
|
|
58
|
+
let _ = execute!(stdout(), SetCursorStyle::SteadyBar);
|
|
63
59
|
let result = app.run(&mut terminal);
|
|
64
60
|
ratatui::restore();
|
|
65
61
|
let _ = execute!(
|
package/src/tui/commands.rs
CHANGED
|
@@ -120,6 +120,20 @@ pub(super) const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
120
120
|
keep_open: true,
|
|
121
121
|
help: true,
|
|
122
122
|
},
|
|
123
|
+
CommandHint {
|
|
124
|
+
insert: "generate-languages ",
|
|
125
|
+
display: "/generate-languages <list|all>",
|
|
126
|
+
desc_key: "cmd_generate_languages",
|
|
127
|
+
keep_open: true,
|
|
128
|
+
help: true,
|
|
129
|
+
},
|
|
130
|
+
CommandHint {
|
|
131
|
+
insert: "generate-ui ",
|
|
132
|
+
display: "/generate-ui <list|all>",
|
|
133
|
+
desc_key: "cmd_generate_ui",
|
|
134
|
+
keep_open: true,
|
|
135
|
+
help: true,
|
|
136
|
+
},
|
|
123
137
|
CommandHint {
|
|
124
138
|
insert: "provider codex",
|
|
125
139
|
display: "/provider codex",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
use crate::text::{byte_index, char_len, compose_hangul_jamo, display_width, prefix};
|
|
2
|
+
use ratatui::layout::{Position, Rect};
|
|
3
|
+
|
|
4
|
+
#[derive(Clone, Debug)]
|
|
5
|
+
pub struct TextEditor {
|
|
6
|
+
lines: Vec<String>,
|
|
7
|
+
row: usize,
|
|
8
|
+
col: usize,
|
|
9
|
+
scroll: usize,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl Default for TextEditor {
|
|
13
|
+
fn default() -> Self {
|
|
14
|
+
Self {
|
|
15
|
+
lines: vec![String::new()],
|
|
16
|
+
row: 0,
|
|
17
|
+
col: 0,
|
|
18
|
+
scroll: 0,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl TextEditor {
|
|
24
|
+
pub fn set_text(&mut self, text: &str) {
|
|
25
|
+
self.lines = text.split('\n').map(str::to_string).collect();
|
|
26
|
+
if text.ends_with('\n') {
|
|
27
|
+
self.lines.pop();
|
|
28
|
+
self.lines.push(String::new());
|
|
29
|
+
}
|
|
30
|
+
if self.lines.is_empty() {
|
|
31
|
+
self.lines.push(String::new());
|
|
32
|
+
}
|
|
33
|
+
self.row = 0;
|
|
34
|
+
self.col = 0;
|
|
35
|
+
self.scroll = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn text(&self) -> String {
|
|
39
|
+
self.lines.join("\n")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) fn visible_text(&mut self, height: usize) -> String {
|
|
43
|
+
if self.row < self.scroll {
|
|
44
|
+
self.scroll = self.row;
|
|
45
|
+
} else if height > 0 && self.row >= self.scroll + height {
|
|
46
|
+
self.scroll = self.row + 1 - height;
|
|
47
|
+
}
|
|
48
|
+
let line_width = ((self.lines.len().max(1)).to_string().len()).max(3);
|
|
49
|
+
self.lines
|
|
50
|
+
.iter()
|
|
51
|
+
.enumerate()
|
|
52
|
+
.skip(self.scroll)
|
|
53
|
+
.take(height.max(1))
|
|
54
|
+
.map(|(index, line)| {
|
|
55
|
+
let cursor = if index == self.row { ">" } else { " " };
|
|
56
|
+
format!("{cursor}{:>width$} {line}", index + 1, width = line_width)
|
|
57
|
+
})
|
|
58
|
+
.collect::<Vec<_>>()
|
|
59
|
+
.join("\n")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(super) fn cursor_position(&self, area: Rect) -> Option<Position> {
|
|
63
|
+
if self.row < self.scroll {
|
|
64
|
+
return None;
|
|
65
|
+
}
|
|
66
|
+
let visible_row = self.row - self.scroll;
|
|
67
|
+
let inner_height = area.height.saturating_sub(2) as usize;
|
|
68
|
+
if visible_row >= inner_height {
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
let line_width = ((self.lines.len().max(1)).to_string().len()).max(3);
|
|
72
|
+
let prefix_width = 1 + line_width + 1;
|
|
73
|
+
let line = self.lines.get(self.row)?;
|
|
74
|
+
let text_before_cursor = prefix(line, self.col);
|
|
75
|
+
let x = area
|
|
76
|
+
.x
|
|
77
|
+
.saturating_add(1)
|
|
78
|
+
.saturating_add((prefix_width + display_width(&text_before_cursor)) as u16)
|
|
79
|
+
.min(area.right().saturating_sub(2));
|
|
80
|
+
let y = area.y.saturating_add(1).saturating_add(visible_row as u16);
|
|
81
|
+
Some(Position::new(x, y))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn insert_char(&mut self, char: char) {
|
|
85
|
+
self.ensure_cursor();
|
|
86
|
+
let byte = byte_index(&self.lines[self.row], self.col);
|
|
87
|
+
self.lines[self.row].insert(byte, char);
|
|
88
|
+
self.col += 1;
|
|
89
|
+
self.normalize_current_line();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub fn insert_newline(&mut self) {
|
|
93
|
+
self.ensure_cursor();
|
|
94
|
+
let byte = byte_index(&self.lines[self.row], self.col);
|
|
95
|
+
let rest = self.lines[self.row].split_off(byte);
|
|
96
|
+
self.lines.insert(self.row + 1, rest);
|
|
97
|
+
self.row += 1;
|
|
98
|
+
self.col = 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn backspace(&mut self) {
|
|
102
|
+
self.ensure_cursor();
|
|
103
|
+
if self.col > 0 {
|
|
104
|
+
let start = byte_index(&self.lines[self.row], self.col - 1);
|
|
105
|
+
let end = byte_index(&self.lines[self.row], self.col);
|
|
106
|
+
self.lines[self.row].replace_range(start..end, "");
|
|
107
|
+
self.col -= 1;
|
|
108
|
+
self.normalize_current_line();
|
|
109
|
+
} else if self.row > 0 {
|
|
110
|
+
let current = self.lines.remove(self.row);
|
|
111
|
+
self.row -= 1;
|
|
112
|
+
self.col = char_len(&self.lines[self.row]);
|
|
113
|
+
self.lines[self.row].push_str(¤t);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub(super) fn delete(&mut self) {
|
|
118
|
+
self.ensure_cursor();
|
|
119
|
+
if self.col < char_len(&self.lines[self.row]) {
|
|
120
|
+
let start = byte_index(&self.lines[self.row], self.col);
|
|
121
|
+
let end = byte_index(&self.lines[self.row], self.col + 1);
|
|
122
|
+
self.lines[self.row].replace_range(start..end, "");
|
|
123
|
+
self.normalize_current_line();
|
|
124
|
+
} else if self.row + 1 < self.lines.len() {
|
|
125
|
+
let next = self.lines.remove(self.row + 1);
|
|
126
|
+
self.lines[self.row].push_str(&next);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub(super) fn move_left(&mut self) {
|
|
131
|
+
if self.col > 0 {
|
|
132
|
+
self.col -= 1;
|
|
133
|
+
} else if self.row > 0 {
|
|
134
|
+
self.row -= 1;
|
|
135
|
+
self.col = char_len(&self.lines[self.row]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub(super) fn move_right(&mut self) {
|
|
140
|
+
if self.col < char_len(&self.lines[self.row]) {
|
|
141
|
+
self.col += 1;
|
|
142
|
+
} else if self.row + 1 < self.lines.len() {
|
|
143
|
+
self.row += 1;
|
|
144
|
+
self.col = 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub(super) fn move_up(&mut self) {
|
|
149
|
+
if self.row > 0 {
|
|
150
|
+
self.row -= 1;
|
|
151
|
+
self.col = self.col.min(char_len(&self.lines[self.row]));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub(super) fn move_down(&mut self) {
|
|
156
|
+
if self.row + 1 < self.lines.len() {
|
|
157
|
+
self.row += 1;
|
|
158
|
+
self.col = self.col.min(char_len(&self.lines[self.row]));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn ensure_cursor(&mut self) {
|
|
163
|
+
if self.lines.is_empty() {
|
|
164
|
+
self.lines.push(String::new());
|
|
165
|
+
}
|
|
166
|
+
self.row = self.row.min(self.lines.len() - 1);
|
|
167
|
+
self.col = self.col.min(char_len(&self.lines[self.row]));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn normalize_current_line(&mut self) {
|
|
171
|
+
let normalized = compose_hangul_jamo(&self.lines[self.row]);
|
|
172
|
+
if normalized == self.lines[self.row] {
|
|
173
|
+
self.col = self.col.min(char_len(&self.lines[self.row]));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
let old_prefix = prefix(&self.lines[self.row], self.col);
|
|
177
|
+
self.lines[self.row] = normalized;
|
|
178
|
+
self.col = char_len(&compose_hangul_jamo(&old_prefix)).min(char_len(&self.lines[self.row]));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
use crate::core::{Problem, localized, normalize_ui_language, ui_text};
|
|
2
|
+
use ratatui::{
|
|
3
|
+
style::{Color, Modifier, Style},
|
|
4
|
+
text::{Line, Span, Text},
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
pub(super) fn render(problem: &Problem, ui_language: &str, light: bool) -> Text<'static> {
|
|
8
|
+
let lang = normalize_ui_language(ui_language);
|
|
9
|
+
let title_style = if light {
|
|
10
|
+
Style::default()
|
|
11
|
+
.fg(Color::Blue)
|
|
12
|
+
.add_modifier(Modifier::BOLD)
|
|
13
|
+
} else {
|
|
14
|
+
Style::default()
|
|
15
|
+
.fg(Color::Yellow)
|
|
16
|
+
.add_modifier(Modifier::BOLD)
|
|
17
|
+
};
|
|
18
|
+
let section_style = if light {
|
|
19
|
+
Style::default()
|
|
20
|
+
.fg(Color::Magenta)
|
|
21
|
+
.add_modifier(Modifier::BOLD)
|
|
22
|
+
} else {
|
|
23
|
+
Style::default()
|
|
24
|
+
.fg(Color::Cyan)
|
|
25
|
+
.add_modifier(Modifier::BOLD)
|
|
26
|
+
};
|
|
27
|
+
let body_style = if light {
|
|
28
|
+
Style::default().fg(Color::Black)
|
|
29
|
+
} else {
|
|
30
|
+
Style::default().fg(Color::Rgb(229, 231, 235))
|
|
31
|
+
};
|
|
32
|
+
let meta_style = if light {
|
|
33
|
+
Style::default().fg(Color::Rgb(75, 85, 99))
|
|
34
|
+
} else {
|
|
35
|
+
Style::default().fg(Color::Rgb(156, 163, 175))
|
|
36
|
+
};
|
|
37
|
+
let code_style = if light {
|
|
38
|
+
Style::default()
|
|
39
|
+
.fg(Color::Black)
|
|
40
|
+
.bg(Color::Rgb(229, 231, 235))
|
|
41
|
+
} else {
|
|
42
|
+
Style::default()
|
|
43
|
+
.fg(Color::Rgb(243, 244, 246))
|
|
44
|
+
.bg(Color::Rgb(31, 41, 55))
|
|
45
|
+
};
|
|
46
|
+
let number = problem
|
|
47
|
+
.id
|
|
48
|
+
.split_once('-')
|
|
49
|
+
.map(|(number, _)| number)
|
|
50
|
+
.unwrap_or(&problem.id);
|
|
51
|
+
let mut lines = vec![
|
|
52
|
+
Line::from(Span::styled(
|
|
53
|
+
format!("{number}. {}", localized(&problem.title, &lang)),
|
|
54
|
+
title_style,
|
|
55
|
+
)),
|
|
56
|
+
Line::from(Span::styled(
|
|
57
|
+
format!(
|
|
58
|
+
"{}: {} {}: {}",
|
|
59
|
+
ui_text(&lang, "difficulty"),
|
|
60
|
+
problem.difficulty,
|
|
61
|
+
ui_text(&lang, "topics"),
|
|
62
|
+
problem.topics.join(", ")
|
|
63
|
+
),
|
|
64
|
+
meta_style,
|
|
65
|
+
)),
|
|
66
|
+
];
|
|
67
|
+
lines.push(Line::default());
|
|
68
|
+
for line in localized(&problem.statement, &lang).trim_end().lines() {
|
|
69
|
+
lines.push(Line::from(Span::styled(line.to_string(), body_style)));
|
|
70
|
+
}
|
|
71
|
+
push_problem_section(
|
|
72
|
+
&mut lines,
|
|
73
|
+
ui_text(&lang, "input"),
|
|
74
|
+
&localized(&problem.input, &lang),
|
|
75
|
+
section_style,
|
|
76
|
+
body_style,
|
|
77
|
+
);
|
|
78
|
+
push_problem_section(
|
|
79
|
+
&mut lines,
|
|
80
|
+
ui_text(&lang, "output"),
|
|
81
|
+
&localized(&problem.output, &lang),
|
|
82
|
+
section_style,
|
|
83
|
+
body_style,
|
|
84
|
+
);
|
|
85
|
+
lines.push(Line::default());
|
|
86
|
+
lines.push(Line::from(Span::styled(
|
|
87
|
+
ui_text(&lang, "examples").to_string(),
|
|
88
|
+
section_style,
|
|
89
|
+
)));
|
|
90
|
+
for (index, case) in problem.examples.iter().enumerate() {
|
|
91
|
+
lines.push(Line::from(Span::styled(
|
|
92
|
+
format!(" {} {}", ui_text(&lang, "example"), index + 1),
|
|
93
|
+
meta_style.add_modifier(Modifier::BOLD),
|
|
94
|
+
)));
|
|
95
|
+
lines.push(Line::from(Span::styled(
|
|
96
|
+
format!(" {}", ui_text(&lang, "input")),
|
|
97
|
+
meta_style,
|
|
98
|
+
)));
|
|
99
|
+
push_code_lines(&mut lines, &case.input, code_style);
|
|
100
|
+
lines.push(Line::from(Span::styled(
|
|
101
|
+
format!(" {}", ui_text(&lang, "output")),
|
|
102
|
+
meta_style,
|
|
103
|
+
)));
|
|
104
|
+
push_code_lines(&mut lines, &case.output, code_style);
|
|
105
|
+
}
|
|
106
|
+
Text::from(lines)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn push_problem_section(
|
|
110
|
+
lines: &mut Vec<Line<'static>>,
|
|
111
|
+
title: &str,
|
|
112
|
+
body: &str,
|
|
113
|
+
section_style: Style,
|
|
114
|
+
body_style: Style,
|
|
115
|
+
) {
|
|
116
|
+
lines.push(Line::default());
|
|
117
|
+
lines.push(Line::from(Span::styled(title.to_string(), section_style)));
|
|
118
|
+
for line in body.trim_end().lines() {
|
|
119
|
+
lines.push(Line::from(Span::styled(format!(" {line}"), body_style)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn push_code_lines(lines: &mut Vec<Line<'static>>, body: &str, code_style: Style) {
|
|
124
|
+
let body = body.trim_end();
|
|
125
|
+
if body.is_empty() {
|
|
126
|
+
lines.push(Line::from(vec![
|
|
127
|
+
Span::raw(" "),
|
|
128
|
+
Span::styled("<empty>".to_string(), code_style),
|
|
129
|
+
]));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for line in body.lines() {
|
|
133
|
+
lines.push(Line::from(vec![
|
|
134
|
+
Span::raw(" "),
|
|
135
|
+
Span::styled(line.to_string(), code_style),
|
|
136
|
+
]));
|
|
137
|
+
}
|
|
138
|
+
}
|