Skip to main content

teksilo_widgets/code_editor/
config.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Injected, language-agnostic editing configuration.
5//!
6//! Everything a code editor does that *looks* language-specific is a mechanism
7//! here plus a value the application supplies. The editor knows how to toggle a
8//! line comment; it does not know that Rust uses `//`. It knows how to close a
9//! bracket; it does not know that Rust has `<>` in generics and Python does not.
10//!
11//! This is the difference between a widget and an IDE, and it is why there is
12//! no `Language` enum anywhere in this module: adding one would mean every new
13//! language is a change to Teksilo rather than a value in the caller's code.
14
15/// How a line's leading indentation is written.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum IndentStyle {
18    /// `width` spaces per indent level.
19    Spaces(u8),
20    /// One tab character per level, rendered `width` columns wide.
21    Tabs { width: u8 },
22}
23
24impl IndentStyle {
25    /// The text one indent level inserts.
26    pub fn unit(&self) -> String {
27        match self {
28            Self::Spaces(n) => " ".repeat(*n as usize),
29            Self::Tabs { .. } => "\t".to_string(),
30        }
31    }
32
33    /// How many columns one level occupies on screen. Both styles need this:
34    /// spaces to know how many to strip on dedent, tabs to render the stop.
35    pub fn width(&self) -> u8 {
36        match self {
37            Self::Spaces(n) => *n,
38            Self::Tabs { width } => *width,
39        }
40    }
41}
42
43impl Default for IndentStyle {
44    /// Four spaces. Chosen because it is the majority default across editors
45    /// and is unambiguous on every renderer; a project that disagrees says so.
46    fn default() -> Self {
47        Self::Spaces(4)
48    }
49}
50
51/// A pair of characters the editor treats as opening and closing delimiters.
52///
53/// Used for auto-closing and for match highlighting. The application declares
54/// the set, because the *same* character means different things per language:
55/// `<` is a bracket in a generic parameter list and a less-than sign in
56/// arithmetic, and only the caller knows which document this is.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct BracketPair {
59    pub open: char,
60    pub close: char,
61}
62
63impl BracketPair {
64    pub const fn new(open: char, close: char) -> Self {
65        Self { open, close }
66    }
67}
68
69/// The three pairs that are structural in essentially every bracketed
70/// language. A convenience starting point, not a default — an editor with no
71/// configured pairs simply does no bracket handling, which is correct for
72/// prose or a log.
73pub const COMMON_BRACKETS: &[BracketPair] = &[
74    BracketPair::new('(', ')'),
75    BracketPair::new('[', ']'),
76    BracketPair::new('{', '}'),
77];
78
79/// Editing behaviour the code editor applies, all supplied by the application.
80#[derive(Debug, Clone)]
81pub struct CodeConfig {
82    /// How indentation is written and how wide a level is.
83    pub indent: IndentStyle,
84    /// Whether Enter carries the current line's leading whitespace onto the
85    /// new line.
86    pub auto_indent: bool,
87    /// Token that starts a line comment (`"//"`, `"#"`, `"--"`, `";"`).
88    /// `None` disables `CodeCommand::ToggleLineComment` entirely rather than
89    /// guessing.
90    pub line_comment: Option<String>,
91    /// Delimiter pairs for auto-closing and match highlighting. Empty disables
92    /// both.
93    pub brackets: Vec<BracketPair>,
94    /// Whether typing an opening delimiter inserts its closing partner.
95    pub auto_close_brackets: bool,
96    /// Whether the delimiter matching the caret's is highlighted.
97    pub match_brackets: bool,
98}
99
100impl Default for CodeConfig {
101    /// The language-neutral default: indent and auto-indent work (they need no
102    /// language knowledge), while comment toggling and bracket handling stay
103    /// **off** because they cannot be right without the application saying what
104    /// the tokens are. A wrong guess here is worse than nothing — inserting `//`
105    /// into a Python file corrupts it silently.
106    fn default() -> Self {
107        Self {
108            indent: IndentStyle::default(),
109            auto_indent: true,
110            line_comment: None,
111            brackets: Vec::new(),
112            auto_close_brackets: false,
113            match_brackets: false,
114        }
115    }
116}
117
118impl CodeConfig {
119    /// The closing partner for `open`, if it is a configured opening delimiter.
120    pub fn closing_for(&self, open: char) -> Option<char> {
121        self.brackets
122            .iter()
123            .find(|p| p.open == open)
124            .map(|p| p.close)
125    }
126
127    /// The opening partner for `close`, if it is a configured closing delimiter.
128    pub fn opening_for(&self, close: char) -> Option<char> {
129        self.brackets
130            .iter()
131            .find(|p| p.close == close)
132            .map(|p| p.open)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn spaces_indent_unit_is_its_width() {
142        assert_eq!(IndentStyle::Spaces(4).unit(), "    ");
143        assert_eq!(IndentStyle::Spaces(2).unit(), "  ");
144    }
145
146    /// A tab is one character however wide it renders — conflating the two is
147    /// the classic indent bug (deleting 4 columns eats 4 tabs).
148    #[test]
149    fn tab_indent_unit_is_one_character_regardless_of_width() {
150        assert_eq!(IndentStyle::Tabs { width: 8 }.unit(), "\t");
151        assert_eq!(IndentStyle::Tabs { width: 8 }.width(), 8);
152    }
153
154    /// The default must not pretend to know the language. Guessing `//` would
155    /// silently corrupt a Python file the first time someone hit Ctrl+/.
156    #[test]
157    fn the_default_config_makes_no_language_assumptions() {
158        let c = CodeConfig::default();
159        assert!(c.line_comment.is_none(), "must not guess a comment token");
160        assert!(c.brackets.is_empty(), "must not guess bracket pairs");
161        assert!(!c.auto_close_brackets);
162        assert!(!c.match_brackets);
163        // These two need no language knowledge, so they are on.
164        assert!(c.auto_indent);
165        assert_eq!(c.indent, IndentStyle::Spaces(4));
166    }
167
168    #[test]
169    fn bracket_lookup_resolves_both_directions() {
170        let c = CodeConfig {
171            brackets: COMMON_BRACKETS.to_vec(),
172            ..CodeConfig::default()
173        };
174        assert_eq!(c.closing_for('('), Some(')'));
175        assert_eq!(c.opening_for('}'), Some('{'));
176        assert_eq!(c.closing_for('<'), None, "unconfigured pairs stay unknown");
177    }
178}