Skip to main content

ravenrs_widgets/
code_editor.rs

1//! An editor of source code: a monospaced text area with line numbers, colors by
2//! language, and lines marked where something is wrong.
3
4use 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/// The language a [`CodeEditor`] colors.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
18pub enum CodeLanguage {
19    /// No colors.
20    Plain,
21    /// Rust: keywords, types, strings, numbers, comments, attributes and macros.
22    #[default]
23    Rust,
24}
25
26/// Something found at a place in the code: what [`CodeEditor::diagnostics`] marks and
27/// what a `Playground` lists under its editor.
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
30pub struct CodeDiagnostic {
31    /// The line, from 1.
32    pub line: usize,
33    /// The column of the first character, from 1; 0 marks the whole line.
34    pub column: usize,
35    /// How many characters it covers; 0 marks the whole line.
36    pub length: usize,
37    /// What happened.
38    pub message: String,
39    /// How to fix it, when there is a likely way.
40    pub hint: Option<String>,
41    /// [`Tone::Danger`] for errors, [`Tone::Warning`] for warnings; other tones read as notes.
42    pub tone: Tone,
43}
44
45#[cfg_attr(feature = "live", ravenrs_live::expose)]
46impl CodeDiagnostic {
47    /// An error at `line` and `column` (both from 1), covering `length` characters.
48    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    /// A warning at `line` and `column` (both from 1), covering `length` characters.
53    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    /// How to fix it.
58    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/// A source code editor: monospaced, no wrapping, line numbers, colors for the
75/// language, Tab and Shift+Tab to indent, and Enter keeping the indentation. Ctrl or Cmd
76/// with Enter is left to shortcuts, so a surrounding `Playground` can run the
77/// code. Diagnostics mark their lines and underline what they cover.
78///
79/// ```
80/// use ravenrs_reactive::State;
81/// use ravenrs_widgets::{CodeDiagnostic, CodeEditor, Ui};
82///
83/// let source = State::new("fn main() {\n    println!(\"hi\");\n}".to_owned());
84/// let editor = CodeEditor::new(source.binding())
85///     .diagnostics(vec![CodeDiagnostic::error(2, 5, 8, "unknown macro")])
86///     .rows(8);
87/// let mut ui = Ui::new(editor, ravenrs_style::Theme::raven_dark());
88/// ui.update();
89/// ```
90#[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    /// An editor of `value`, as Rust, with line numbers and an indent of 4 spaces.
114    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    /// The language it colors.
128    pub fn language(mut self, language: CodeLanguage) -> Self {
129        self.language = language;
130        self
131    }
132
133    /// Shows or hides the line numbers.
134    pub fn line_numbers(mut self, shown: bool) -> Self {
135        self.line_numbers = shown;
136        self
137    }
138
139    /// Marks these places.
140    pub fn diagnostics(mut self, diagnostics: Vec<CodeDiagnostic>) -> Self {
141        self.diagnostics = diagnostics;
142        self
143    }
144
145    /// How many spaces Tab inserts; at least 1.
146    pub fn indent(mut self, spaces: usize) -> Self {
147        self.indent = spaces.max(1);
148        self
149    }
150
151    /// How many lines are visible; more scroll. 12 by default.
152    pub fn rows(mut self, rows: usize) -> Self {
153        self.rows = rows.max(1);
154        self
155    }
156
157    /// Takes the height its parent gives, instead of [`Self::rows`].
158    pub fn fill(mut self) -> Self {
159        self.fill = true;
160        self
161    }
162
163    /// Shows the code without letting it change; it can still be selected and copied.
164    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
204/// The colored stretches of Rust code, as char indices. It reads tokens, not syntax, so
205/// it never fails: unfinished strings and comments color to the end.
206pub 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}