Skip to main content

teksilo_widgets/
code_editor.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Multi-line plain-text and code editing surfaces.
5//!
6//! Three faces over one core:
7//!
8//! - `CodeEditor` — a source editor: gutter, current-line highlight,
9//!   indentation, bracket handling, multiple carets.
10//! - `PlainTextEditor` — the same core with the code affordances off and
11//!   wrapping on: a notes field, a commit message, a description box.
12//! - `LogView` — read-only, append-only, tail-following.
13//!
14//! They are one implementation because they differ in *configuration*, not in
15//! kind. All three are a monospaced-or-not run of lines with a caret in it; a
16//! separate widget per face would triplicate the caret, selection, IME,
17//! clipboard, scrolling, and accessibility and let them drift.
18//!
19//! # Why not `RichTextEditor`
20//!
21//! `RichTextEditor` already edits multi-line text, and this deliberately does
22//! not build on it. Its command vocabulary is tables, lists, blockquotes, and
23//! bold — reusing it would put Tab-navigates-a-table-cell and
24//! Ctrl+B-emboldens into a source file, where the first is wrong and the second
25//! is meaningless. Its state carries a table-aware Ctrl+A ladder and a rich
26//! clipboard fragment; this one carries an indent policy and a caret vector.
27//! The overlap is real but it is the *clock* — the caret blink, the debounce
28//! window, the scroll arithmetic — and that lives in the crate-internal
29//! `common::editor_runtime`, shared by both.
30//!
31//! # Language-agnostic by construction
32//!
33//! There is no `Language` enum here. Comment tokens, bracket pairs, indent
34//! width, and highlighting are [`CodeConfig`] values the application supplies:
35//! the editor knows how to toggle a line comment, not that Rust uses `//`.
36//! Guessing would be worse than not knowing — inserting `//` into a Python file
37//! corrupts it silently.
38
39mod a11y;
40mod clipboard;
41mod completion;
42mod config;
43mod frame_loop;
44mod gutter;
45mod keyboard;
46mod log_stream;
47mod log_view;
48mod mouse;
49mod policy;
50mod semantics;
51mod state;
52mod widget;
53
54#[cfg(test)]
55mod tests;
56
57pub use completion::{CompletionContext, CompletionItem, CompletionKind};
58pub use config::{BracketPair, COMMON_BRACKETS, CodeConfig, IndentStyle};
59pub use log_view::{LogView, LogViewHandle};
60pub use policy::{CODE_EDITOR_PRESET, CODE_READ_ONLY_PRESET, CodeCommand};
61pub use widget::{CodeEditor, PlainTextEditor};
62
63use std::rc::Rc;
64
65use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
66use teksilo_core::accessibility::AccessNodeBuilder;
67use teksilo_core::build_context::BuildContext;
68use teksilo_core::widget::{LayoutContext, PaintContext, Widget, WidgetPlacement};
69use teksilo_core::widget_id::WidgetId;
70use teksilo_text::text_document::TextDocument;
71use teksilo_text::{RichTextEngine, SharedTypesetter, WrapMode};
72
73use self::state::{CodeEditorState, SharedState};
74use crate::common::editor_runtime::PolicyBundle;
75use crate::rich_text::paint::{PaintParams, paint_frame};
76
77/// The paint-only leaf that renders the document.
78///
79/// Split from the wrapper for the same reason the rich text editor is: the
80/// wrapper owns focus, handlers, and style-supplied chrome, so the body can be
81/// a pure leaf that an application's custom style may place anywhere inside its
82/// decoration without the focus semantics moving with it. The two are joined
83/// only by the shared state — neither holds a reference to the other.
84pub(crate) struct CodeEditorBody {
85    state: SharedState,
86    min_lines: Option<u32>,
87    max_lines: Option<u32>,
88}
89
90impl std::fmt::Debug for CodeEditorBody {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("CodeEditorBody")
93            .field("policy", &self.state.borrow().policy)
94            .finish_non_exhaustive()
95    }
96}
97
98impl Widget for CodeEditorBody {
99    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
100        use teksilo_core::binding::BindingLevel;
101
102        let self_id = ctx.self_id();
103        let registry = ctx.binding_registry();
104
105        let st = self.state.borrow();
106
107        // The caret is painted here, so its every toggle must mark *this* node
108        // for repaint. Skipped when the policy never draws one.
109        if st.policy.caret_policy != crate::common::editor_runtime::CaretPolicy::Hidden {
110            st.caret_visible
111                .bind_to(self_id, registry, BindingLevel::RepaintOnly);
112        }
113
114        // An edit must both repaint and re-walk the accessibility tree — this
115        // body is the node carrying the role and the paragraph/run children.
116        st.document_version
117            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
118        st.document_version
119            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
120
121        // The completion popup's open/selection state rides on this node's a11y
122        // (expanded / controls / active_descendant), so re-walk when it changes.
123        st.completion
124            .open
125            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
126        st.completion
127            .selected
128            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
129
130        // Scroll never changes the AT tree, so it is repaint-only.
131        for sig in [&st.scroll_x, &st.scroll_y] {
132            sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
133        }
134        // Caret and selection are repaint-only for geometry — they never change
135        // this widget's size — but they ALSO change what the a11y walk reports
136        // via `set_text_selection_to`. A caret-only move (arrow key, click,
137        // drag-select) emits no document event, so `document_version` never
138        // bumps; without an `AccessibilityOnly` binding here `a11y_dirty` would
139        // never flip and a screen reader would hear the caret frozen at the last
140        // edit. Binding one signal at two levels is the same pattern
141        // `document_version` uses above. `has_selection` is derived from the
142        // caret and anchor, so binding those two covers every selection change.
143        st.cursor_position
144            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
145        st.cursor_position
146            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
147        st.cursor_anchor
148            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
149        st.cursor_anchor
150            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
151        st.has_selection
152            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
153        // A caret added or removed changes what is drawn but not the layout.
154        st.caret_count
155            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
156
157        Vec::new()
158    }
159
160    fn layout_response(
161        &self,
162        proposal: SizeProposal,
163        ctx: &LayoutContext,
164    ) -> teksilo_core::widget::LayoutResponse {
165        let w = proposal.width.unwrap_or(200.0).max(0.0);
166
167        // Greedy: fill whatever we are given. The editor is normally the
168        // scrollable region of a pane, so it takes the space and scrolls.
169        if self.min_lines.is_none() && self.max_lines.is_none() {
170            let h = proposal.height.unwrap_or(100.0).max(0.0);
171            return Size::new(w, h).into();
172        }
173
174        // Intrinsic: size to content, clamped to [min_lines, max_lines] — the
175        // composer pattern, where the field grows with what is typed until it
176        // is allowed to grow no further and starts scrolling.
177        let st = self.state.borrow();
178        let line_scale = if st.follow_text_scale {
179            ctx.text_scale
180        } else {
181            1.0
182        };
183        let line_h = st.engine.default_line_height() * line_scale;
184        let content_h = st.engine.content_height();
185        drop(st);
186
187        let min_h = self.min_lines.map(|n| n as f32 * line_h).unwrap_or(0.0);
188        let max_h = self
189            .max_lines
190            .map(|n| n as f32 * line_h)
191            .unwrap_or(f32::INFINITY);
192        Size::new(w, content_h.clamp(min_h, max_h).max(0.0)).into()
193    }
194
195    fn place_children(
196        &self,
197        bounds: Rect,
198        _proposal: SizeProposal,
199        _children: &mut [WidgetPlacement],
200        _ctx: &LayoutContext,
201    ) {
202        // A leaf, but layout runs before paint, so this is the earliest — and
203        // therefore authoritative — point at which the viewport is known.
204        self.state.borrow_mut().sync_viewport(bounds);
205    }
206
207    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
208        use crate::common::editor_runtime::CaretPolicy;
209
210        let mut st = self.state.borrow_mut();
211
212        // Resolve the app's colour overrides against the live theme each paint,
213        // so a theme swap or a Signal-bound colour reaches the glyphs. A
214        // changed colour forces a full render because the cached glyph quads
215        // have the old colour baked in.
216        let new_text = match &st.text_color_prop {
217            Some(p) => p.resolve(ctx.theme, true).to_array(),
218            None => ctx.theme.colors.editor_fg.to_array(),
219        };
220        st.engine.set_text_color(new_text);
221        if st.last_text_color != Some(new_text) {
222            st.last_text_color = Some(new_text);
223            st.pending_full_render = true;
224        }
225
226        let new_caret = match &st.caret_color_prop {
227            Some(p) => p.resolve(ctx.theme, true).to_array(),
228            None => ctx.theme.colors.editor_caret.to_array(),
229        };
230        st.engine.set_cursor_color(new_caret);
231        if st.last_cursor_color != Some(new_caret) {
232            st.last_cursor_color = Some(new_caret);
233            st.pending_full_render = true;
234        }
235
236        // Selection desaturates in an inactive window unless the app pinned a
237        // colour — the same convention every desktop selection follows.
238        let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
239            p.resolve(ctx.theme, true).to_array()
240        } else if ctx.window_active {
241            ctx.theme.colors.editor_selection_bg.to_array()
242        } else {
243            ctx.theme.colors.selection_bg_inactive.to_array()
244        };
245        if st.last_selection_color != Some(new_sel) {
246            st.engine.set_selection_color(new_sel);
247            st.last_selection_color = Some(new_sel);
248            st.pending_full_render = true;
249        }
250
251        // Logical font scale: a11y × per-editor `font_size_scale`. Changes
252        // glyph advances, so it forces a relayout, not just a re-render.
253        let target_scale = st.effective_font_scale(ctx.text_scale);
254        if st.last_font_scale.is_nan() || (st.last_font_scale - target_scale).abs() > f32::EPSILON {
255            st.last_font_scale = target_scale;
256            st.engine.set_font_scale(target_scale);
257            st.needs_full_layout = true;
258            st.pending_full_render = true;
259        }
260
261        // Idempotent echo of place_children, which already adopted these exact
262        // bounds. Kept because paint is reachable on a first frame where layout
263        // has run but the frame loop has not yet ticked.
264        st.sync_viewport(bounds);
265
266        let did_full_layout = st.needs_full_layout || !st.engine.has_full_layout();
267        if did_full_layout {
268            let flow = st.document.snapshot_flow();
269            st.engine.layout_full(&flow);
270            st.needs_full_layout = false;
271            st.content_dirty = true;
272        }
273
274        let caret_on = match st.policy.caret_policy {
275            CaretPolicy::Hidden => false,
276            CaretPolicy::StaticVisible => st.has_focus && st.window_active,
277            CaretPolicy::Blinking => st.caret_visible.get() && st.has_focus && st.window_active,
278        };
279
280        // Publish every caret to the engine in one call. A single-caret editor
281        // is just the one-element case, so there is no second code path to keep
282        // in step.
283        let cursors: Vec<teksilo_text::CursorDisplay> = st
284            .all_carets()
285            .map(|c| teksilo_text::CursorDisplay {
286                position: c.position(),
287                anchor: c.anchor(),
288                affinity: st.cursor_affinity,
289                visible: caret_on,
290                selected_cells: Vec::new(),
291            })
292            .collect();
293        st.engine.set_cursors(&cursors);
294
295        let scroll_y = st.scroll_y.get();
296        st.engine.set_scroll_offset(scroll_y);
297
298        // Cull the render to the visible clip band when the editor is laid out at
299        // full document height inside an outer scroller (opt-in, off by default).
300        // `clip_bounds` is the on-screen slice an ancestor clip leaves visible; we
301        // map it into content space and render only that band (plus a half-viewport
302        // margin), the same window the rich text editor uses. Never moves glyph
303        // positions or hit-testing — it only limits what is emitted.
304        let render_window = if st.window_to_clip {
305            ctx.clip_bounds.map(|clip| {
306                let vis_top = (scroll_y + (clip.y - bounds.y)).max(0.0);
307                let vis_h = clip.height.max(0.0);
308                let margin = vis_h * 0.5;
309                ((vis_top - margin).max(0.0), vis_h + 2.0 * margin)
310            })
311        } else {
312            None
313        };
314        st.engine.set_render_window(render_window);
315
316        canvas.set_clip(bounds);
317
318        let pending_full = std::mem::replace(&mut st.pending_full_render, false);
319        let block_relayout = st.last_relayout_block_id.take();
320
321        let state_ref: &mut CodeEditorState = &mut st;
322        let CodeEditorState {
323            ref mut engine,
324            ref document,
325            ref mut image_cache,
326            ..
327        } = *state_ref;
328        let paint_closure = |frame: &teksilo_text::RenderFrame| {
329            paint_frame(
330                canvas,
331                PaintParams {
332                    frame,
333                    origin: Point::new(bounds.x, bounds.y),
334                    document,
335                    image_cache,
336                    // No inline images on this surface, so none can be missing.
337                    image_resolver: None,
338                    selection: None,
339                    selection_color: [0.0; 4],
340                    selected_image_out: None,
341                    resize_preview: None,
342                    draw_caret: caret_on,
343                },
344            );
345        };
346        if did_full_layout || pending_full {
347            engine.with_render_frame(paint_closure);
348        } else if let Some(bid) = block_relayout {
349            engine.with_render_block_only(bid, paint_closure);
350        } else {
351            engine.with_render_cursor_only(paint_closure);
352        }
353
354        canvas.clear_clip();
355    }
356
357    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
358        let st = self.state.borrow();
359
360        // Role, read-only, the paragraph/run tree, selection reporting, and the
361        // text actions — the walk shared with the log view (full-document here).
362        a11y::build_editor_a11y(&st, builder);
363
364        // Completion popup — the ARIA combobox-with-listbox pattern (as ComboBox
365        // and SearchField): the editor keeps focus and carries has-popup +
366        // autocomplete, announces expanded (both branches, so it never sticks
367        // open), and — only while shown, or a stale reference can crash a screen
368        // reader — points controls at the listbox and active-descendant at the
369        // highlighted row.
370        if st.completion.has_provider() {
371            use teksilo_core::accessibility::widget_id_to_node_id;
372            use teksilo_core::accesskit::{AutoComplete, HasPopup};
373
374            let inner = builder.inner_mut();
375            inner.set_has_popup(HasPopup::Listbox);
376            inner.set_auto_complete(AutoComplete::List);
377            let open = st.completion.is_open();
378            inner.set_expanded(open);
379            if open {
380                if let Some(pid) = st.completion.panel_id {
381                    inner.push_controlled(widget_id_to_node_id(pid));
382                }
383                if let Some(row) = st.completion.active_row.get() {
384                    inner.set_active_descendant(widget_id_to_node_id(row));
385                }
386            }
387        }
388    }
389
390    fn clips_children(&self) -> bool {
391        true
392    }
393}
394
395/// Shared construction for every face of the editor.
396///
397/// Returns the state handle; the public builders wrap it. Keeping this one
398/// function is what makes `CodeEditor` / `PlainTextEditor` / `LogView`
399/// genuinely the same core rather than three that merely look alike.
400pub(crate) fn construct(
401    document: TextDocument,
402    policy: PolicyBundle,
403    config: CodeConfig,
404    wrap_mode: WrapMode,
405) -> SharedState {
406    // A private engine to begin with. `build()` swaps in one sharing the
407    // application's typesetter when there is one, so glyphs land in the atlas
408    // the renderer uploads; headless tests have no typesetter and the private
409    // engine is then exactly right, since no renderer is ever invoked.
410    let mut engine = RichTextEngine::private_default();
411    engine.set_wrap_mode(wrap_mode);
412    // No hyphenation, ever: it is a prose affordance, and hyphenating source
413    // code would break identifiers across lines.
414    CodeEditorState::new(document, engine, policy, config, wrap_mode)
415}
416
417/// Swap the private engine for one sharing the application's typesetter.
418///
419/// Called from the wrapper's `build`. Outside a windowed app there is no
420/// typesetter and the private engine stays, which is why the headless tests
421/// exercise the same paths.
422pub(crate) fn adopt_shared_typesetter(state: &SharedState, ctx: &mut BuildContext) {
423    let Some(shared) = ctx.app_state::<SharedTypesetter>() else {
424        return;
425    };
426    let mut st = state.borrow_mut();
427    let wrap = st.wrap_mode;
428    let typography = st.engine.typography_defaults().clone();
429    let mut engine = RichTextEngine::from_shared(shared.clone());
430    engine.set_wrap_mode(wrap);
431    engine.set_typography_defaults(typography);
432    st.engine = engine;
433    st.needs_full_layout = true;
434}
435
436/// Build the paint-only body for a state handle.
437pub(crate) fn body_for(
438    state: &SharedState,
439    min_lines: Option<u32>,
440    max_lines: Option<u32>,
441) -> CodeEditorBody {
442    CodeEditorBody {
443        state: state.clone(),
444        min_lines,
445        max_lines,
446    }
447}
448
449/// Publish cursor state onto the reactive signals.
450///
451/// Every mutating path ends here. The signals are written *after* the state
452/// borrow is dropped: `Signal::set` fans out to observers synchronously, and an
453/// observer that reaches back into the widget would panic on a live borrow.
454pub(crate) fn sync_cursor_signals(state: &SharedState) {
455    let mut st = state.borrow_mut();
456    let pos = st.cursor.position();
457    let anchor = st.cursor.anchor();
458    let has_sel = st.all_carets().any(|c| c.has_selection());
459    let count = 1 + st.extra_carets.len();
460
461    let pos_sig = st.cursor_position.clone();
462    let anchor_sig = st.cursor_anchor.clone();
463    let sel_sig = st.has_selection.clone();
464    let count_sig = st.caret_count.clone();
465    let caret_vis = st.caret_visible.clone();
466
467    // Recompute the bracket match at the single choke point every caret move
468    // passes through — but only when the app asked for it, so a plain-text
469    // editor or a document with no configured pairs pays nothing. The scan reads
470    // the document while it is borrowed here; the resulting signal is written
471    // after the borrow drops, with the rest.
472    let bracket_sig = st.bracket_match.clone();
473    let bracket_val = if st.config.match_brackets {
474        semantics::current_bracket_match(&st)
475    } else {
476        None
477    };
478
479    // Restart the blink so the caret stays lit through a held arrow key rather
480    // than toggling mid-motion. `restart` deliberately does not write the
481    // signal — see its docs — so the caller does, below, outside the borrow.
482    let blink_reset = st.has_focus
483        && matches!(
484            st.policy.caret_policy,
485            crate::common::editor_runtime::CaretPolicy::Blinking
486        );
487    if blink_reset {
488        st.blink.restart();
489    }
490    drop(st);
491
492    pos_sig.set_if_changed(pos);
493    anchor_sig.set_if_changed(anchor);
494    sel_sig.set_if_changed(has_sel);
495    count_sig.set_if_changed(count);
496    bracket_sig.set_if_changed(bracket_val);
497    if blink_reset {
498        caret_vis.set_if_changed(true);
499    }
500}
501
502/// A handle onto a live editor, cloneable and detachable from the widget.
503///
504/// The `EditorHandle` pattern: an app keeps one to drive the editor from a
505/// toolbar, a shortcut, or a test without holding the widget itself.
506#[derive(Clone)]
507pub struct CodeEditorHandle {
508    state: SharedState,
509}
510
511impl std::fmt::Debug for CodeEditorHandle {
512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513        f.debug_struct("CodeEditorHandle").finish_non_exhaustive()
514    }
515}
516
517impl CodeEditorHandle {
518    pub(crate) fn new(state: SharedState) -> Self {
519        Self { state }
520    }
521
522    /// The caret's document position.
523    pub fn cursor_position(&self) -> usize {
524        self.state.borrow().cursor.position()
525    }
526
527    /// The primary caret's document position — a character offset into the whole
528    /// document, not a line or column — as a reactive signal. Bind it in a status
529    /// bar to show a caret position that tracks every caret move, not only edits.
530    pub fn cursor_position_signal(&self) -> teksilo_core::Signal<usize> {
531        self.state.borrow().cursor_position.clone()
532    }
533
534    /// Live caret count — `1` unless multi-caret editing is active.
535    pub fn caret_count(&self) -> teksilo_core::Signal<usize> {
536        self.state.borrow().caret_count.clone()
537    }
538
539    /// The bracket next to the caret and its match, as document positions, or
540    /// `None`. Populated only when the editor was configured with
541    /// `match_brackets` and bracket pairs; a status surface can bind it, or an
542    /// app can read it to drive its own overlay.
543    pub fn bracket_match(&self) -> teksilo_core::Signal<Option<(usize, usize)>> {
544        self.state.borrow().bracket_match.clone()
545    }
546
547    pub fn has_selection(&self) -> teksilo_core::Signal<bool> {
548        self.state.borrow().has_selection.clone()
549    }
550
551    pub fn can_undo(&self) -> teksilo_core::Signal<bool> {
552        self.state.borrow().can_undo.clone()
553    }
554
555    /// Undo this editor's last edit.
556    ///
557    /// The handle could report [`can_undo`](Self::can_undo) long before it could
558    /// *act* on it, which left a host able to light an Undo button here and
559    /// unable to make it do anything. Ctrl+Z inside the widget always worked;
560    /// this is the same command from outside.
561    pub fn undo(&self) {
562        let st = self.state.borrow();
563        let _ = st.document.undo();
564    }
565
566    /// Redo this editor's last undone edit.
567    pub fn redo(&self) {
568        let st = self.state.borrow();
569        let _ = st.document.redo();
570    }
571
572    /// Copy the selection to the clipboard.
573    pub fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
574        clipboard::copy(&self.state.borrow(), ctx);
575    }
576
577    /// Cut the selection to the clipboard.
578    pub fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
579        clipboard::cut(&mut self.state.borrow_mut(), ctx);
580    }
581
582    /// Paste over the selection.
583    pub fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
584        clipboard::paste(&mut self.state.borrow_mut(), ctx);
585    }
586
587    /// Select the whole document.
588    pub fn select_all(&self) {
589        let st = self.state.borrow();
590        st.cursor
591            .select(teksilo_text::text_document::SelectionType::Document);
592    }
593
594    /// Is this editor refusing edits?
595    pub fn is_read_only(&self) -> bool {
596        self.state.borrow().policy.is_read_only()
597    }
598
599    pub fn can_redo(&self) -> teksilo_core::Signal<bool> {
600        self.state.borrow().can_redo.clone()
601    }
602
603    /// Bumps on every content or format change.
604    pub fn document_version(&self) -> teksilo_core::Signal<u64> {
605        self.state.borrow().document_version.clone()
606    }
607
608    pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
609        self.state.borrow().scroll_y.clone()
610    }
611
612    #[cfg(test)]
613    pub(crate) fn state_handle(&self) -> SharedState {
614        self.state.clone()
615    }
616}
617
618/// Keep `Rc` in the import set for the state alias.
619const _: () = {
620    fn _assert_shared(_: &Rc<std::cell::RefCell<CodeEditorState>>) {}
621};
622
623// ── The framework's uniform view of a text-editing widget ────────────────────
624
625impl teksilo_core::text_surface::TextSurface for CodeEditorHandle {
626    fn can_undo(&self) -> bool {
627        CodeEditorHandle::can_undo(self).get()
628    }
629
630    fn can_redo(&self) -> bool {
631        CodeEditorHandle::can_redo(self).get()
632    }
633
634    fn undo(&self) {
635        CodeEditorHandle::undo(self);
636    }
637
638    fn redo(&self) {
639        CodeEditorHandle::redo(self);
640    }
641
642    fn has_selection(&self) -> bool {
643        CodeEditorHandle::has_selection(self).get()
644    }
645
646    fn is_read_only(&self) -> bool {
647        CodeEditorHandle::is_read_only(self)
648    }
649
650    fn allows_copy(&self) -> bool {
651        self.state.borrow().policy.clipboard_policy.allows_copy()
652    }
653
654    fn history_frozen(&self) -> bool {
655        !self
656            .state
657            .borrow()
658            .policy
659            .command_filter
660            .accepts(policy::CodeCommand::Undo)
661    }
662
663    fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
664        CodeEditorHandle::cut(self, ctx);
665    }
666
667    fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
668        CodeEditorHandle::copy(self, ctx);
669    }
670
671    fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
672        CodeEditorHandle::paste(self, ctx);
673    }
674
675    /// Code has no rich formatting to strip; the plain paste is the paste.
676    fn paste_plain(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
677        CodeEditorHandle::paste(self, ctx);
678    }
679
680    fn select_all(&self) {
681        CodeEditorHandle::select_all(self);
682    }
683}