1use std::fmt;
5use std::rc::Rc;
6
7use ravenrs_reactive::Binding;
8use ravenrs_style::Semantic;
9
10use crate::context::{BuildCx, NodeId};
11use crate::editors::{CodeLook, Highlighter, LineMark};
12use crate::{TextArea, Tone, View};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
18pub enum CodeLanguage {
19 Plain,
21 #[default]
23 Rust,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
30pub struct CodeDiagnostic {
31 pub line: usize,
33 pub column: usize,
35 pub length: usize,
37 pub message: String,
39 pub hint: Option<String>,
41 pub tone: Tone,
43}
44
45#[cfg_attr(feature = "live", ravenrs_live::expose)]
46impl CodeDiagnostic {
47 pub fn error(line: usize, column: usize, length: usize, message: impl Into<String>) -> Self {
49 Self { line, column, length, message: message.into(), hint: None, tone: Tone::Danger }
50 }
51
52 pub fn warning(line: usize, column: usize, length: usize, message: impl Into<String>) -> Self {
54 Self { tone: Tone::Warning, ..Self::error(line, column, length, message) }
55 }
56
57 pub fn hint(mut self, hint: impl Into<String>) -> Self {
59 self.hint = Some(hint.into());
60 self
61 }
62}
63
64pub(crate) fn tone_color(tone: Tone) -> Semantic {
65 match tone {
66 Tone::Danger => Semantic::Danger,
67 Tone::Warning => Semantic::Warning,
68 Tone::Success => Semantic::Success,
69 Tone::Accent => Semantic::Accent,
70 Tone::Neutral => Semantic::InkMuted,
71 }
72}
73
74#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
91pub struct CodeEditor {
92 value: Binding<String>,
93 language: CodeLanguage,
94 line_numbers: bool,
95 diagnostics: Vec<CodeDiagnostic>,
96 indent: usize,
97 rows: usize,
98 fill: bool,
99 read_only: bool,
100}
101
102impl fmt::Debug for CodeEditor {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("CodeEditor")
105 .field("language", &self.language)
106 .field("diagnostics", &self.diagnostics.len())
107 .finish_non_exhaustive()
108 }
109}
110
111#[cfg_attr(feature = "live", ravenrs_live::expose)]
112impl CodeEditor {
113 pub fn new(value: Binding<String>) -> Self {
115 Self {
116 value,
117 language: CodeLanguage::Rust,
118 line_numbers: true,
119 diagnostics: Vec::new(),
120 indent: 4,
121 rows: 12,
122 fill: false,
123 read_only: false,
124 }
125 }
126
127 pub fn language(mut self, language: CodeLanguage) -> Self {
129 self.language = language;
130 self
131 }
132
133 pub fn line_numbers(mut self, shown: bool) -> Self {
135 self.line_numbers = shown;
136 self
137 }
138
139 pub fn diagnostics(mut self, diagnostics: Vec<CodeDiagnostic>) -> Self {
141 self.diagnostics = diagnostics;
142 self
143 }
144
145 pub fn indent(mut self, spaces: usize) -> Self {
147 self.indent = spaces.max(1);
148 self
149 }
150
151 pub fn rows(mut self, rows: usize) -> Self {
153 self.rows = rows.max(1);
154 self
155 }
156
157 pub fn fill(mut self) -> Self {
159 self.fill = true;
160 self
161 }
162
163 pub fn read_only(mut self, read_only: bool) -> Self {
165 self.read_only = read_only;
166 self
167 }
168}
169
170impl View for CodeEditor {
171 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
172 let highlight: Option<Highlighter> = match self.language {
173 CodeLanguage::Plain => None,
174 CodeLanguage::Rust => Some(Rc::new(highlight_rust)),
175 };
176 let marks: Vec<LineMark> = self
177 .diagnostics
178 .iter()
179 .filter(|d| d.line > 0)
180 .map(|d| {
181 let columns = (d.column > 0 && d.length > 0).then(|| (d.column - 1, d.column - 1 + d.length));
182 (d.line - 1, tone_color(d.tone), columns)
183 })
184 .collect();
185 let look = CodeLook {
186 highlight,
187 line_numbers: self.line_numbers,
188 marks: Rc::new(marks),
189 indent: self.indent,
190 fill: self.fill,
191 background: Semantic::Surface,
192 };
193 let area = TextArea::new(self.value.clone()).rows(self.rows).read_only(self.read_only).code(look);
194 area.build(cx)
195 }
196}
197
198const RUST_KEYWORDS: &[&str] = &[
199 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false",
200 "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
201 "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while",
202];
203
204pub fn highlight_rust(text: &str) -> Vec<(usize, usize, Semantic)> {
207 let chars: Vec<char> = text.chars().collect();
208 let mut spans = Vec::new();
209 let mut i = 0;
210 let mut attribute_depth = 0usize;
211 while i < chars.len() {
212 let c = chars[i];
213 let start = i;
214 let next = chars.get(i + 1).copied();
215 let semantic = if c == '/' && next == Some('/') {
216 while i < chars.len() && chars[i] != '\n' {
217 i += 1;
218 }
219 Some(Semantic::InkMuted)
220 } else if c == '/' && next == Some('*') {
221 i += 2;
222 while i < chars.len() && !(chars[i] == '*' && chars.get(i + 1) == Some(&'/')) {
223 i += 1;
224 }
225 i = (i + 2).min(chars.len());
226 Some(Semantic::InkMuted)
227 } else if c == '"' {
228 i += 1;
229 while i < chars.len() && chars[i] != '"' {
230 i += if chars[i] == '\\' { 2 } else { 1 };
231 }
232 i = (i + 1).min(chars.len());
233 Some(Semantic::Success)
234 } else if c == '\'' && chars.get(i + 2) == Some(&'\'') {
235 i += 3;
236 Some(Semantic::Success)
237 } else if c == '#' && (next == Some('[') || (next == Some('!') && chars.get(i + 2) == Some(&'['))) {
238 i += if next == Some('!') { 3 } else { 2 };
239 attribute_depth = 1;
240 Some(Semantic::Chart4)
241 } else if attribute_depth > 0 && (c == '[' || c == ']') {
242 attribute_depth = if c == '[' { attribute_depth + 1 } else { attribute_depth - 1 };
243 i += 1;
244 Some(Semantic::Chart4)
245 } else if c.is_ascii_digit() {
246 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '.')
247 {
248 if chars[i] == '.' && !chars.get(i + 1).is_some_and(char::is_ascii_digit) {
249 break;
250 }
251 i += 1;
252 }
253 Some(Semantic::Chart3)
254 } else if c.is_alphabetic() || c == '_' {
255 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
256 i += 1;
257 }
258 let word: String = chars[start..i].iter().collect();
259 if attribute_depth > 0 {
260 Some(Semantic::Chart4)
261 } else if chars.get(i) == Some(&'!') && chars.get(i + 1) != Some(&'=') {
262 i += 1;
263 Some(Semantic::Chart4)
264 } else if RUST_KEYWORDS.contains(&word.as_str()) {
265 Some(Semantic::FocusRing)
266 } else if word.starts_with(char::is_uppercase) {
267 Some(Semantic::Chart2)
268 } else {
269 None
270 }
271 } else {
272 i += 1;
273 (attribute_depth > 0 && !c.is_whitespace()).then_some(Semantic::Chart4)
274 };
275 if let Some(semantic) = semantic {
276 match spans.last_mut() {
277 Some((_, end, last)) if *end == start && *last == semantic => *end = i,
278 _ => spans.push((start, i, semantic)),
279 }
280 }
281 }
282 spans
283}