Skip to main content

iced_code_editor/canvas_editor/
mod.rs

1//! Canvas-based text editor widget for maximum performance.
2//!
3//! This module provides a custom Canvas widget that handles all text rendering
4//! and input directly, bypassing Iced's higher-level widgets for optimal speed.
5
6use iced::advanced::text::{
7    Alignment, Paragraph, Renderer as TextRenderer, Text,
8};
9use iced::widget::operation::{RelativeOffset, snap_to};
10use iced::widget::{Id, canvas};
11use std::cell::{Cell, RefCell};
12use std::cmp::Ordering as CmpOrdering;
13use std::ops::Range;
14use std::rc::Rc;
15use std::sync::atomic::{AtomicU64, Ordering};
16#[cfg(not(target_arch = "wasm32"))]
17use std::time::Instant;
18use unicode_width::UnicodeWidthChar;
19
20use crate::i18n::Translations;
21use crate::text_buffer::TextBuffer;
22use crate::theme::Style;
23pub use history::CommandHistory;
24
25#[cfg(target_arch = "wasm32")]
26use web_time::Instant;
27
28/// Global counter for generating unique editor IDs (starts at 1)
29static EDITOR_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
30
31/// ID of the currently focused editor (0 = no editor focused)
32static FOCUSED_EDITOR_ID: AtomicU64 = AtomicU64::new(0);
33
34// Re-export submodules
35mod canvas_impl;
36mod clipboard;
37pub mod command;
38mod cursor;
39pub(crate) mod cursor_set;
40pub mod history;
41pub mod ime_requester;
42pub mod lsp;
43#[cfg(all(feature = "lsp-process", not(target_arch = "wasm32")))]
44pub mod lsp_process;
45mod search;
46mod search_dialog;
47mod selection;
48mod update;
49mod view;
50mod wrapping;
51
52/// Canvas-based text editor constants
53pub(crate) const FONT_SIZE: f32 = 14.0;
54pub(crate) const LINE_HEIGHT: f32 = 20.0;
55pub(crate) const CHAR_WIDTH: f32 = 8.4; // Monospace character width
56pub(crate) const TAB_WIDTH: usize = 4;
57pub(crate) const GUTTER_WIDTH: f32 = 45.0;
58pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
59    std::time::Duration::from_millis(530);
60
61/// Measures the width of a single character.
62///
63/// # Arguments
64///
65/// * `c` - The character to measure
66/// * `full_char_width` - The width of a full-width character
67/// * `char_width` - The width of the character
68///
69/// # Returns
70///
71/// The calculated width of the character as a `f32`
72pub(crate) fn measure_char_width(
73    c: char,
74    full_char_width: f32,
75    char_width: f32,
76) -> f32 {
77    if c == '\t' {
78        return char_width * TAB_WIDTH as f32;
79    }
80    match c.width() {
81        Some(w) if w > 1 => full_char_width,
82        Some(_) => char_width,
83        None => 0.0,
84    }
85}
86
87/// Measures rendered text width, accounting for CJK wide characters.
88///
89/// - Wide characters (e.g. Chinese) use FONT_SIZE.
90/// - Narrow characters (e.g. Latin) use CHAR_WIDTH.
91/// - Control characters (except tab) have width 0.
92///
93/// # Arguments
94///
95/// * `text` - The text string to measure
96/// * `full_char_width` - The width of a full-width character
97/// * `char_width` - The width of a regular character
98///
99/// # Returns
100///
101/// The total calculated width of the text as a `f32`
102pub(crate) fn measure_text_width(
103    text: &str,
104    full_char_width: f32,
105    char_width: f32,
106) -> f32 {
107    text.chars()
108        .map(|c| measure_char_width(c, full_char_width, char_width))
109        .sum()
110}
111
112/// Epsilon value for floating-point comparisons in text layout.
113pub(crate) const EPSILON: f32 = 0.001;
114/// Multiplier used to extend the cached render window beyond the visible range.
115/// The cache window margin is computed as:
116///     margin = visible_lines_count * CACHE_WINDOW_MARGIN_MULTIPLIER
117/// A larger margin reduces how often we clear and rebuild the canvas cache when
118/// scrolling, improving performance on very large files while still ensuring
119/// correct initial rendering during the first scroll.
120pub(crate) const CACHE_WINDOW_MARGIN_MULTIPLIER: usize = 2;
121
122/// Compares two floating point numbers with a small epsilon tolerance.
123///
124/// # Arguments
125///
126/// * `a` - first float number
127/// * `b` - second float number
128///
129/// # Returns
130///
131/// * `Ordering::Equal` if `abs(a - b) < EPSILON`
132/// * `Ordering::Greater` if `a > b` (and not equal)
133/// * `Ordering::Less` if `a < b` (and not equal)
134pub(crate) fn compare_floats(a: f32, b: f32) -> CmpOrdering {
135    if (a - b).abs() < EPSILON {
136        CmpOrdering::Equal
137    } else if a > b {
138        CmpOrdering::Greater
139    } else {
140        CmpOrdering::Less
141    }
142}
143
144#[derive(Debug, Clone)]
145pub(crate) struct ImePreedit {
146    pub(crate) content: String,
147    pub(crate) selection: Option<Range<usize>>,
148}
149
150/// Canvas-based high-performance text editor.
151pub struct CodeEditor {
152    /// Unique ID for this editor instance (for focus management)
153    pub(crate) editor_id: u64,
154    /// Text buffer
155    pub(crate) buffer: TextBuffer,
156    /// All cursor positions (multi-cursor support).
157    pub(crate) cursors: cursor_set::CursorSet,
158    /// Horizontal scroll offset in pixels, only used when wrap_enabled = false
159    pub(crate) horizontal_scroll_offset: f32,
160    /// Editor theme style
161    pub(crate) style: Style,
162    /// Syntax highlighting language
163    pub(crate) syntax: String,
164    /// Last cursor blink time
165    pub(crate) last_blink: Instant,
166    /// Cursor visible state
167    pub(crate) cursor_visible: bool,
168    /// Mouse is currently dragging for selection
169    pub(crate) is_dragging: bool,
170    /// Cached geometry for the "content" layer.
171    ///
172    /// This layer includes expensive-to-build, mostly static visuals such as:
173    /// - syntax-highlighted text glyphs
174    /// - line numbers / gutter text
175    ///
176    /// It is intentionally kept stable across selection/cursor movement so
177    /// that mouse-drag selection feels smooth.
178    pub(crate) content_cache: canvas::Cache,
179    /// Cached geometry for the "overlay" layer.
180    ///
181    /// This layer includes visuals that change frequently without modifying the
182    /// underlying buffer, such as:
183    /// - cursor and current-line highlight
184    /// - selection highlight
185    /// - search match highlights
186    /// - IME preedit decorations
187    ///
188    /// Keeping overlays in a separate cache avoids invalidating the content
189    /// layer on every cursor blink or selection drag.
190    pub(crate) overlay_cache: canvas::Cache,
191    /// Scrollable ID for programmatic scrolling
192    pub(crate) scrollable_id: Id,
193    /// ID for the horizontal scrollable widget (only used when wrap_enabled = false)
194    pub(crate) horizontal_scrollable_id: Id,
195    /// Cache for max content width: (buffer_revision, width_in_pixels)
196    pub(crate) max_content_width_cache: RefCell<Option<(u64, f32)>>,
197    /// Current viewport scroll position (Y offset)
198    pub(crate) viewport_scroll: f32,
199    /// Viewport height (visible area)
200    pub(crate) viewport_height: f32,
201    /// Viewport width (visible area)
202    pub(crate) viewport_width: f32,
203    /// Command history for undo/redo
204    pub(crate) history: CommandHistory,
205    /// Whether we're currently grouping commands (for smart undo)
206    pub(crate) is_grouping: bool,
207    /// Line wrapping enabled
208    pub(crate) wrap_enabled: bool,
209    /// Auto-indentation enabled
210    pub(crate) auto_indent_enabled: bool,
211    /// Indentation style (spaces or tab)
212    pub(crate) indent_style: IndentStyle,
213    /// Wrap column (None = wrap at viewport width)
214    pub(crate) wrap_column: Option<usize>,
215    /// Search state
216    pub(crate) search_state: search::SearchState,
217    /// Translations for UI text
218    pub(crate) translations: Translations,
219    /// Whether search/replace functionality is enabled
220    pub(crate) search_replace_enabled: bool,
221    /// Whether line numbers are displayed
222    pub(crate) line_numbers_enabled: bool,
223    /// Whether LSP support is enabled
224    pub(crate) lsp_enabled: bool,
225    /// Active LSP client connection, if configured.
226    pub(crate) lsp_client: Option<Box<dyn lsp::LspClient>>,
227    /// Metadata for the currently open LSP document.
228    pub(crate) lsp_document: Option<lsp::LspDocument>,
229    /// Pending incremental LSP text changes not yet flushed.
230    pub(crate) lsp_pending_changes: Vec<lsp::LspTextChange>,
231    /// Shadow copy of buffer content used to compute LSP deltas.
232    pub(crate) lsp_shadow_text: String,
233    /// Whether to auto-flush LSP changes after edits.
234    pub(crate) lsp_auto_flush: bool,
235    /// Whether the canvas has user input focus (for keyboard events)
236    pub(crate) has_canvas_focus: bool,
237    /// Whether input processing is locked to prevent focus stealing
238    pub(crate) focus_locked: bool,
239    /// Whether to show the cursor (for rendering)
240    pub(crate) show_cursor: bool,
241    /// Current keyboard modifiers state (Ctrl, Alt, Shift, Logo).
242    ///
243    /// This is updated via subscription events and used to handle modifier-dependent
244    /// interactions, such as "Ctrl+Click" for jumping to a definition.
245    pub(crate) modifiers: Cell<iced::keyboard::Modifiers>,
246    /// The font used for rendering text
247    pub(crate) font: iced::Font,
248    /// IME pre-edit state (for CJK input)
249    pub(crate) ime_preedit: Option<ImePreedit>,
250    /// Font size in pixels
251    pub(crate) font_size: f32,
252    /// Full character width (wide chars like CJK) in pixels
253    pub(crate) full_char_width: f32,
254    /// Line height in pixels
255    pub(crate) line_height: f32,
256    /// Character width in pixels
257    pub(crate) char_width: f32,
258    /// Cached render window: the first visual line index included in the cache.
259    /// We keep a larger window than the currently visible range to avoid clearing
260    /// the canvas cache on every small scroll. Only when scrolling crosses the
261    /// window boundary do we re-window and clear the cache.
262    pub(crate) last_first_visible_line: usize,
263    /// Cached render window start line (inclusive)
264    pub(crate) cache_window_start_line: usize,
265    /// Cached render window end line (exclusive)
266    pub(crate) cache_window_end_line: usize,
267    /// Monotonic revision counter for buffer content.
268    ///
269    /// Any operation that changes the buffer must bump this counter to
270    /// invalidate derived layout caches (e.g. wrapping / visual lines). The
271    /// exact value is not semantically meaningful, so `wrapping_add` is used to
272    /// avoid overflow panics while still producing a different key.
273    pub(crate) buffer_revision: u64,
274    /// Cached result of line wrapping ("visual lines") for the current layout key.
275    ///
276    /// This is stored behind a `RefCell` because wrapping is needed during
277    /// rendering (where we only have `&self`), but we still want to memoize the
278    /// expensive computation without forcing external mutability.
279    visual_lines_cache: RefCell<Option<VisualLinesCache>>,
280}
281
282#[derive(Clone, Copy, PartialEq, Eq)]
283struct VisualLinesKey {
284    buffer_revision: u64,
285    /// `f32::to_bits()` is used so the cache key is stable and exact:
286    /// - no epsilon comparisons are required
287    /// - NaN payloads (if any) do not collapse unexpectedly
288    viewport_width_bits: u32,
289    gutter_width_bits: u32,
290    wrap_enabled: bool,
291    wrap_column: Option<usize>,
292    full_char_width_bits: u32,
293    char_width_bits: u32,
294}
295
296struct VisualLinesCache {
297    key: VisualLinesKey,
298    visual_lines: Rc<Vec<wrapping::VisualLine>>,
299}
300
301/// Messages emitted by the code editor
302#[derive(Debug, Clone)]
303pub enum Message {
304    /// Character typed
305    CharacterInput(char),
306    /// Backspace pressed
307    Backspace,
308    /// Delete pressed
309    Delete,
310    /// Enter pressed
311    Enter,
312    /// Tab pressed (inserts 4 spaces)
313    Tab,
314    /// Arrow key pressed (direction, shift_pressed)
315    ArrowKey(ArrowDirection, bool),
316    /// Mouse clicked at position
317    MouseClick(iced::Point),
318    /// Mouse drag for selection
319    MouseDrag(iced::Point),
320    /// Mouse moved within the editor without dragging
321    MouseHover(iced::Point),
322    /// Mouse released
323    MouseRelease,
324    /// Copy selected text (Ctrl+C)
325    Copy,
326    /// Paste text from clipboard (Ctrl+V)
327    Paste(String),
328    /// Delete selected text (Shift+Delete)
329    DeleteSelection,
330    /// Request redraw for cursor blink
331    Tick,
332    /// Page Up pressed
333    PageUp,
334    /// Page Down pressed
335    PageDown,
336    /// Home key pressed (move to start of line, shift_pressed)
337    Home(bool),
338    /// End key pressed (move to end of line, shift_pressed)
339    End(bool),
340    /// Ctrl+Home pressed (move to start of document)
341    CtrlHome,
342    /// Ctrl+End pressed (move to end of document)
343    CtrlEnd,
344    /// Go to an explicit logical position (line, column), both 0-based.
345    GotoPosition(usize, usize),
346    /// Viewport scrolled - track scroll position
347    Scrolled(iced::widget::scrollable::Viewport),
348    /// Horizontal scrollbar scrolled (only when wrap is disabled)
349    HorizontalScrolled(iced::widget::scrollable::Viewport),
350    /// Undo last operation (Ctrl+Z)
351    Undo,
352    /// Redo last undone operation (Ctrl+Y)
353    Redo,
354    /// Open search dialog (Ctrl+F)
355    OpenSearch,
356    /// Open search and replace dialog (Ctrl+H)
357    OpenSearchReplace,
358    /// Close search dialog (Escape)
359    CloseSearch,
360    /// Search query text changed
361    SearchQueryChanged(String),
362    /// Replace text changed
363    ReplaceQueryChanged(String),
364    /// Toggle case sensitivity
365    ToggleCaseSensitive,
366    /// Find next match (F3)
367    FindNext,
368    /// Find previous match (Shift+F3)
369    FindPrevious,
370    /// Replace current match
371    ReplaceNext,
372    /// Replace all matches
373    ReplaceAll,
374    /// Tab pressed in search dialog (cycle forward)
375    SearchDialogTab,
376    /// Shift+Tab pressed in search dialog (cycle backward)
377    SearchDialogShiftTab,
378    /// Tab pressed for focus navigation (when search dialog is not open)
379    FocusNavigationTab,
380    /// Shift+Tab pressed for focus navigation (when search dialog is not open)
381    FocusNavigationShiftTab,
382    /// Canvas gained focus (mouse click)
383    CanvasFocusGained,
384    /// Canvas lost focus (external widget interaction)
385    CanvasFocusLost,
386    /// Triggered when the user performs a Ctrl+Click (or Cmd+Click on macOS)
387    /// on the editor content, intending to jump to the definition of the symbol
388    /// under the cursor.
389    JumpClick(iced::Point),
390    /// IME input method opened
391    ImeOpened,
392    /// IME pre-edit update (content, selection range)
393    ImePreedit(String, Option<Range<usize>>),
394    /// IME commit text
395    ImeCommit(String),
396    /// IME input method closed
397    ImeClosed,
398    /// Alt+Click: add a new cursor at the given canvas position
399    AltClick(iced::Point),
400    /// Ctrl+Alt+Up: add a cursor on the line above the primary cursor
401    AddCursorAbove,
402    /// Ctrl+Alt+Down: add a cursor on the line below the primary cursor
403    AddCursorBelow,
404    /// Ctrl+D: select the next occurrence of the currently selected text (or word under cursor)
405    SelectNextOccurrence,
406}
407
408/// Indentation style used when pressing the Tab key.
409///
410/// Controls whether indentation inserts spaces or a tab character.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub enum IndentStyle {
413    /// Insert `n` space characters.
414    Spaces(u8),
415    /// Insert a single tab character (`\t`).
416    Tab,
417}
418
419impl IndentStyle {
420    /// All standard indentation styles available for selection.
421    pub const ALL: [IndentStyle; 4] = [
422        IndentStyle::Spaces(2),
423        IndentStyle::Spaces(4),
424        IndentStyle::Spaces(8),
425        IndentStyle::Tab,
426    ];
427}
428
429impl std::fmt::Display for IndentStyle {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        match self {
432            IndentStyle::Spaces(1) => write!(f, "1 space"),
433            IndentStyle::Spaces(n) => write!(f, "{n} spaces"),
434            IndentStyle::Tab => write!(f, "Tab"),
435        }
436    }
437}
438
439/// Arrow key directions
440#[derive(Debug, Clone, Copy)]
441pub enum ArrowDirection {
442    Up,
443    Down,
444    Left,
445    Right,
446}
447
448impl CodeEditor {
449    /// Creates a new canvas-based text editor.
450    ///
451    /// # Arguments
452    ///
453    /// * `content` - Initial text content
454    /// * `syntax` - Syntax highlighting language (e.g., "py", "lua", "rs")
455    ///
456    /// # Returns
457    ///
458    /// A new `CodeEditor` instance
459    pub fn new(content: &str, syntax: &str) -> Self {
460        // Generate a unique ID for this editor instance
461        let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
462
463        // Give focus to the first editor created (ID == 1)
464        if editor_id == 1 {
465            FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
466        }
467
468        let mut editor = Self {
469            editor_id,
470            buffer: TextBuffer::new(content),
471            cursors: cursor_set::CursorSet::new((0, 0)),
472            horizontal_scroll_offset: 0.0,
473            style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
474            syntax: syntax.to_string(),
475            last_blink: Instant::now(),
476            cursor_visible: true,
477            is_dragging: false,
478            content_cache: canvas::Cache::default(),
479            overlay_cache: canvas::Cache::default(),
480            scrollable_id: Id::unique(),
481            horizontal_scrollable_id: Id::unique(),
482            max_content_width_cache: RefCell::new(None),
483            viewport_scroll: 0.0,
484            viewport_height: 600.0, // Default, will be updated
485            viewport_width: 800.0,  // Default, will be updated
486            history: CommandHistory::new(100),
487            is_grouping: false,
488            wrap_enabled: true,
489            auto_indent_enabled: true,
490            indent_style: IndentStyle::Spaces(4),
491            wrap_column: None,
492            search_state: search::SearchState::new(),
493            translations: Translations::default(),
494            search_replace_enabled: true,
495            line_numbers_enabled: true,
496            lsp_enabled: true,
497            lsp_client: None,
498            lsp_document: None,
499            lsp_pending_changes: Vec::new(),
500            lsp_shadow_text: String::new(),
501            lsp_auto_flush: true,
502            has_canvas_focus: false,
503            focus_locked: false,
504            show_cursor: false,
505            modifiers: Cell::new(iced::keyboard::Modifiers::default()),
506            font: iced::Font::MONOSPACE,
507            ime_preedit: None,
508            font_size: FONT_SIZE,
509            full_char_width: CHAR_WIDTH * 2.0,
510            line_height: LINE_HEIGHT,
511            char_width: CHAR_WIDTH,
512            // Initialize render window tracking for virtual scrolling:
513            // these indices define the cached visual line window. The window is
514            // expanded beyond the visible range to amortize redraws and keep scrolling smooth.
515            last_first_visible_line: 0,
516            cache_window_start_line: 0,
517            cache_window_end_line: 0,
518            buffer_revision: 0,
519            visual_lines_cache: RefCell::new(None),
520        };
521
522        // Perform initial character dimension calculation
523        editor.recalculate_char_dimensions(false);
524
525        editor
526    }
527
528    /// Sets the font used by the editor
529    ///
530    /// # Arguments
531    ///
532    /// * `font` - The iced font to set for the editor
533    pub fn set_font(&mut self, font: iced::Font) {
534        self.font = font;
535        self.recalculate_char_dimensions(false);
536    }
537
538    /// Sets the font size and recalculates character dimensions.
539    ///
540    /// If `auto_adjust_line_height` is true, `line_height` will also be scaled to maintain
541    /// the default proportion (Line Height ~ 1.43x).
542    ///
543    /// # Arguments
544    ///
545    /// * `size` - The font size in pixels
546    /// * `auto_adjust_line_height` - Whether to automatically adjust the line height
547    pub fn set_font_size(&mut self, size: f32, auto_adjust_line_height: bool) {
548        self.font_size = size;
549        self.recalculate_char_dimensions(auto_adjust_line_height);
550    }
551
552    /// Recalculates character dimensions based on current font and size.
553    fn recalculate_char_dimensions(&mut self, auto_adjust_line_height: bool) {
554        self.char_width = self.measure_single_char_width("a");
555        // Use '汉' as a standard reference for CJK (Chinese, Japanese, Korean) wide characters
556        self.full_char_width = self.measure_single_char_width("汉");
557
558        // Fallback for infinite width measurements
559        if self.char_width.is_infinite() {
560            self.char_width = self.font_size / 2.0; // Rough estimate for monospace
561        }
562
563        if self.full_char_width.is_infinite() {
564            self.full_char_width = self.font_size;
565        }
566
567        if auto_adjust_line_height {
568            let line_height_ratio = LINE_HEIGHT / FONT_SIZE;
569            self.line_height = self.font_size * line_height_ratio;
570        }
571
572        self.content_cache.clear();
573        self.overlay_cache.clear();
574    }
575
576    /// Measures the width of a single character string using the current font settings.
577    fn measure_single_char_width(&self, content: &str) -> f32 {
578        let text = Text {
579            content,
580            font: self.font,
581            size: iced::Pixels(self.font_size),
582            line_height: iced::advanced::text::LineHeight::default(),
583            bounds: iced::Size::new(f32::INFINITY, f32::INFINITY),
584            align_x: Alignment::Left,
585            align_y: iced::alignment::Vertical::Top,
586            shaping: iced::advanced::text::Shaping::Advanced,
587            wrapping: iced::advanced::text::Wrapping::default(),
588        };
589        let p = <iced::Renderer as TextRenderer>::Paragraph::with_text(text);
590        p.min_width()
591    }
592
593    /// Returns the current font size.
594    ///
595    /// # Returns
596    ///
597    /// The font size in pixels
598    pub fn font_size(&self) -> f32 {
599        self.font_size
600    }
601
602    /// Returns the width of a standard narrow character in pixels.
603    ///
604    /// # Returns
605    ///
606    /// The character width in pixels
607    pub fn char_width(&self) -> f32 {
608        self.char_width
609    }
610
611    /// Returns the width of a wide character (e.g. CJK) in pixels.
612    ///
613    /// # Returns
614    ///
615    /// The full character width in pixels
616    pub fn full_char_width(&self) -> f32 {
617        self.full_char_width
618    }
619
620    /// Measures the rendered width for a given text snippet using editor metrics.
621    pub fn measure_text_width(&self, text: &str) -> f32 {
622        measure_text_width(text, self.full_char_width, self.char_width)
623    }
624
625    /// Sets the line height used by the editor
626    ///
627    /// # Arguments
628    ///
629    /// * `height` - The line height in pixels
630    pub fn set_line_height(&mut self, height: f32) {
631        self.line_height = height;
632        self.content_cache.clear();
633        self.overlay_cache.clear();
634    }
635
636    /// Returns the current line height.
637    ///
638    /// # Returns
639    ///
640    /// The line height in pixels
641    pub fn line_height(&self) -> f32 {
642        self.line_height
643    }
644
645    /// Returns the current viewport height in pixels.
646    pub fn viewport_height(&self) -> f32 {
647        self.viewport_height
648    }
649
650    /// Returns the current viewport width in pixels.
651    pub fn viewport_width(&self) -> f32 {
652        self.viewport_width
653    }
654
655    /// Returns the current vertical scroll offset in pixels.
656    pub fn viewport_scroll(&self) -> f32 {
657        self.viewport_scroll
658    }
659
660    /// Returns the current text content as a string.
661    ///
662    /// # Returns
663    ///
664    /// The complete text content of the editor
665    pub fn content(&self) -> String {
666        self.buffer.to_string()
667    }
668
669    /// Sets the viewport height for the editor.
670    ///
671    /// This determines the minimum height of the canvas, ensuring proper
672    /// background rendering even when content is smaller than the viewport.
673    ///
674    /// # Arguments
675    ///
676    /// * `height` - The viewport height in pixels
677    ///
678    /// # Returns
679    ///
680    /// Self for method chaining
681    ///
682    /// # Example
683    ///
684    /// ```
685    /// use iced_code_editor::CodeEditor;
686    ///
687    /// let editor = CodeEditor::new("fn main() {}", "rs")
688    ///     .with_viewport_height(500.0);
689    /// ```
690    #[must_use]
691    pub fn with_viewport_height(mut self, height: f32) -> Self {
692        self.viewport_height = height;
693        self
694    }
695
696    /// Sets the theme style for the editor.
697    ///
698    /// # Arguments
699    ///
700    /// * `style` - The style to apply to the editor
701    ///
702    /// # Example
703    ///
704    /// ```
705    /// use iced_code_editor::{CodeEditor, theme};
706    ///
707    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
708    /// editor.set_theme(theme::from_iced_theme(&iced::Theme::TokyoNightStorm));
709    /// ```
710    pub fn set_theme(&mut self, style: Style) {
711        self.style = style;
712        self.content_cache.clear();
713        self.overlay_cache.clear();
714    }
715
716    /// Sets the language for UI translations.
717    ///
718    /// This changes the language used for all UI text elements in the editor,
719    /// including search dialog tooltips, placeholders, and labels.
720    ///
721    /// # Arguments
722    ///
723    /// * `language` - The language to use for UI text
724    ///
725    /// # Example
726    ///
727    /// ```
728    /// use iced_code_editor::{CodeEditor, Language};
729    ///
730    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
731    /// editor.set_language(Language::French);
732    /// ```
733    pub fn set_language(&mut self, language: crate::i18n::Language) {
734        self.translations.set_language(language);
735        self.overlay_cache.clear();
736    }
737
738    /// Returns the current UI language.
739    ///
740    /// # Returns
741    ///
742    /// The currently active language for UI text
743    ///
744    /// # Example
745    ///
746    /// ```
747    /// use iced_code_editor::{CodeEditor, Language};
748    ///
749    /// let editor = CodeEditor::new("fn main() {}", "rs");
750    /// let current_lang = editor.language();
751    /// ```
752    pub fn language(&self) -> crate::i18n::Language {
753        self.translations.language()
754    }
755
756    /// Attaches an LSP client and opens a document for the current buffer.
757    ///
758    /// This sends an initial `did_open` with the current buffer contents and
759    /// resets any pending LSP change state.
760    ///
761    /// # Arguments
762    ///
763    /// * `client` - The LSP client to notify
764    /// * `document` - Document metadata describing the buffer
765    pub fn attach_lsp(
766        &mut self,
767        mut client: Box<dyn lsp::LspClient>,
768        mut document: lsp::LspDocument,
769    ) {
770        if !self.lsp_enabled {
771            return;
772        }
773        document.version = 1;
774        let text = self.buffer.to_string();
775        client.did_open(&document, &text);
776        self.lsp_client = Some(client);
777        self.lsp_document = Some(document);
778        self.lsp_shadow_text = text;
779        self.lsp_pending_changes.clear();
780    }
781
782    /// Opens a new document on the attached LSP client.
783    ///
784    /// If a document is already open, this will close it before opening the new
785    /// one and reset pending change tracking.
786    ///
787    /// # Arguments
788    ///
789    /// * `document` - Document metadata describing the buffer
790    pub fn lsp_open_document(&mut self, mut document: lsp::LspDocument) {
791        let Some(client) = self.lsp_client.as_mut() else { return };
792        if let Some(current) = self.lsp_document.as_ref() {
793            client.did_close(current);
794        }
795        document.version = 1;
796        let text = self.buffer.to_string();
797        client.did_open(&document, &text);
798        self.lsp_document = Some(document);
799        self.lsp_shadow_text = text;
800        self.lsp_pending_changes.clear();
801    }
802
803    /// Detaches the current LSP client and closes any open document.
804    ///
805    /// This clears all LSP-related state on the editor instance.
806    pub fn detach_lsp(&mut self) {
807        if let (Some(client), Some(document)) =
808            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
809        {
810            client.did_close(document);
811        }
812        self.lsp_client = None;
813        self.lsp_document = None;
814        self.lsp_shadow_text = String::new();
815        self.lsp_pending_changes.clear();
816    }
817
818    /// Sends a `did_save` notification with the current buffer contents.
819    pub fn lsp_did_save(&mut self) {
820        if let (Some(client), Some(document)) =
821            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
822        {
823            let text = self.buffer.to_string();
824            client.did_save(document, &text);
825        }
826    }
827
828    /// Requests hover information at the current cursor position.
829    pub fn lsp_request_hover(&mut self) {
830        let position = self.lsp_position_from_cursor();
831        if let (Some(client), Some(document)) =
832            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
833        {
834            client.request_hover(document, position);
835        }
836    }
837
838    /// Requests hover information at a canvas point.
839    ///
840    /// Returns `true` if the point maps to a valid buffer position and the
841    /// request was sent.
842    pub fn lsp_request_hover_at(&mut self, point: iced::Point) -> bool {
843        let Some(position) = self.lsp_position_from_point(point) else {
844            return false;
845        };
846        if let (Some(client), Some(document)) =
847            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
848        {
849            client.request_hover(document, position);
850            return true;
851        }
852        false
853    }
854
855    /// Requests hover information at an explicit LSP position.
856    ///
857    /// Returns `true` if an LSP client is attached and the request was sent.
858    pub fn lsp_request_hover_at_position(
859        &mut self,
860        position: lsp::LspPosition,
861    ) -> bool {
862        if let (Some(client), Some(document)) =
863            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
864        {
865            client.request_hover(document, position);
866            return true;
867        }
868        false
869    }
870
871    /// Converts a canvas point to an LSP position, if possible.
872    pub fn lsp_position_at_point(
873        &self,
874        point: iced::Point,
875    ) -> Option<lsp::LspPosition> {
876        self.lsp_position_from_point(point)
877    }
878
879    /// Returns the hover anchor position and its canvas point for a given
880    /// cursor location.
881    ///
882    /// The anchor is the start of the word under the cursor, which is useful
883    /// for LSP hover and definition requests.
884    pub fn lsp_hover_anchor_at_point(
885        &self,
886        point: iced::Point,
887    ) -> Option<(lsp::LspPosition, iced::Point)> {
888        let (line, col) = self.calculate_cursor_from_point(point)?;
889        let line_content = self.buffer.line(line);
890        let anchor_col = Self::word_start_in_line(line_content, col);
891        let anchor_point =
892            self.point_from_position(line, anchor_col).unwrap_or(point);
893        let line = u32::try_from(line).unwrap_or(u32::MAX);
894        let character = u32::try_from(anchor_col).unwrap_or(u32::MAX);
895        Some((lsp::LspPosition { line, character }, anchor_point))
896    }
897
898    /// Requests completion items at the current cursor position.
899    pub fn lsp_request_completion(&mut self) {
900        let position = self.lsp_position_from_cursor();
901        if let (Some(client), Some(document)) =
902            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
903        {
904            client.request_completion(document, position);
905        }
906    }
907
908    /// Flushes pending LSP text changes to the attached client.
909    ///
910    /// This increments the document version and sends `did_change` with all
911    /// queued changes.
912    pub fn lsp_flush_pending_changes(&mut self) {
913        if self.lsp_pending_changes.is_empty() {
914            return;
915        }
916
917        if let (Some(client), Some(document)) =
918            (self.lsp_client.as_mut(), self.lsp_document.as_mut())
919        {
920            let changes = std::mem::take(&mut self.lsp_pending_changes);
921            document.version = document.version.saturating_add(1);
922            client.did_change(document, &changes);
923        }
924    }
925
926    /// Sets whether LSP changes are flushed automatically after edits.
927    pub fn set_lsp_auto_flush(&mut self, auto_flush: bool) {
928        self.lsp_auto_flush = auto_flush;
929    }
930
931    /// Requests focus for this editor.
932    ///
933    /// This method programmatically sets the focus to this editor instance,
934    /// allowing it to receive keyboard events. Other editors will automatically
935    /// lose focus.
936    ///
937    /// # Example
938    ///
939    /// ```
940    /// use iced_code_editor::CodeEditor;
941    ///
942    /// let mut editor1 = CodeEditor::new("fn main() {}", "rs");
943    /// let mut editor2 = CodeEditor::new("fn test() {}", "rs");
944    ///
945    /// // Give focus to editor2
946    /// editor2.request_focus();
947    /// ```
948    pub fn request_focus(&self) {
949        FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
950    }
951
952    /// Checks if this editor currently has focus.
953    ///
954    /// Returns `true` if this editor will receive keyboard events,
955    /// `false` otherwise.
956    ///
957    /// # Returns
958    ///
959    /// `true` if focused, `false` otherwise
960    ///
961    /// # Example
962    ///
963    /// ```
964    /// use iced_code_editor::CodeEditor;
965    ///
966    /// let editor = CodeEditor::new("fn main() {}", "rs");
967    /// if editor.is_focused() {
968    ///     println!("Editor has focus");
969    /// }
970    /// ```
971    pub fn is_focused(&self) -> bool {
972        FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
973    }
974
975    /// Resets the editor with new content.
976    ///
977    /// This method replaces the buffer content and resets all editor state
978    /// (cursor position, selection, scroll, history) to initial values.
979    /// Use this instead of creating a new `CodeEditor` instance to ensure
980    /// proper widget tree updates in iced.
981    ///
982    /// Returns a `Task` that scrolls the editor to the top, which also
983    /// forces a redraw of the canvas.
984    ///
985    /// # Arguments
986    ///
987    /// * `content` - The new text content
988    ///
989    /// # Returns
990    ///
991    /// A `Task<Message>` that should be returned from your update function
992    ///
993    /// # Example
994    ///
995    /// ```ignore
996    /// use iced_code_editor::CodeEditor;
997    ///
998    /// let mut editor = CodeEditor::new("initial content", "lua");
999    /// // Later, reset with new content and get the task
1000    /// let task = editor.reset("new content");
1001    /// // Return task.map(YourMessage::Editor) from your update function
1002    /// ```
1003    pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
1004        self.buffer = TextBuffer::new(content);
1005        self.cursors.set_single((0, 0));
1006        self.horizontal_scroll_offset = 0.0;
1007        self.is_dragging = false;
1008        self.viewport_scroll = 0.0;
1009        self.history = CommandHistory::new(100);
1010        self.is_grouping = false;
1011        self.last_blink = Instant::now();
1012        self.cursor_visible = true;
1013        self.content_cache = canvas::Cache::default();
1014        self.overlay_cache = canvas::Cache::default();
1015        self.buffer_revision = self.buffer_revision.wrapping_add(1);
1016        *self.visual_lines_cache.borrow_mut() = None;
1017        self.enqueue_lsp_change();
1018
1019        // Scroll to top to force a redraw
1020        snap_to(self.scrollable_id.clone(), RelativeOffset::START)
1021    }
1022
1023    /// Resets the cursor blink animation.
1024    pub(crate) fn reset_cursor_blink(&mut self) {
1025        self.last_blink = Instant::now();
1026        self.cursor_visible = true;
1027    }
1028
1029    /// Converts the current cursor position into an LSP position.
1030    fn lsp_position_from_cursor(&self) -> lsp::LspPosition {
1031        let pos = self.cursors.primary_position();
1032        let line = u32::try_from(pos.0).unwrap_or(u32::MAX);
1033        let character = u32::try_from(pos.1).unwrap_or(u32::MAX);
1034        lsp::LspPosition { line, character }
1035    }
1036
1037    /// Converts a canvas point into an LSP position, if it hits the buffer.
1038    fn lsp_position_from_point(
1039        &self,
1040        point: iced::Point,
1041    ) -> Option<lsp::LspPosition> {
1042        let (line, col) = self.calculate_cursor_from_point(point)?;
1043        let line = u32::try_from(line).unwrap_or(u32::MAX);
1044        let character = u32::try_from(col).unwrap_or(u32::MAX);
1045        Some(lsp::LspPosition { line, character })
1046    }
1047
1048    /// Converts a logical buffer position into a canvas point, if visible.
1049    fn point_from_position(
1050        &self,
1051        line: usize,
1052        col: usize,
1053    ) -> Option<iced::Point> {
1054        let visual_lines = self.visual_lines_cached(self.viewport_width);
1055        let visual_index = wrapping::WrappingCalculator::logical_to_visual(
1056            &visual_lines,
1057            line,
1058            col,
1059        )?;
1060        let visual_line = &visual_lines[visual_index];
1061        let line_content = self.buffer.line(visual_line.logical_line);
1062        let prefix_len = col.saturating_sub(visual_line.start_col);
1063        let prefix_text: String = line_content
1064            .chars()
1065            .skip(visual_line.start_col)
1066            .take(prefix_len)
1067            .collect();
1068        let x = self.gutter_width()
1069            + 5.0
1070            + measure_text_width(
1071                &prefix_text,
1072                self.full_char_width,
1073                self.char_width,
1074            );
1075        let y = visual_index as f32 * self.line_height;
1076        Some(iced::Point::new(x, y))
1077    }
1078
1079    /// Returns the word-start column in a line for a given column.
1080    ///
1081    /// Word characters include ASCII alphanumerics and underscore.
1082    pub(crate) fn word_start_in_line(line: &str, col: usize) -> usize {
1083        let chars: Vec<char> = line.chars().collect();
1084        if chars.is_empty() {
1085            return 0;
1086        }
1087        let mut idx = col.min(chars.len());
1088        if idx == chars.len() {
1089            idx = idx.saturating_sub(1);
1090        }
1091        if !Self::is_word_char(chars[idx]) {
1092            if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1093                idx -= 1;
1094            } else {
1095                return col.min(chars.len());
1096            }
1097        }
1098        while idx > 0 && Self::is_word_char(chars[idx - 1]) {
1099            idx -= 1;
1100        }
1101        idx
1102    }
1103
1104    /// Returns the word-end column in a line for a given column.
1105    pub(crate) fn word_end_in_line(line: &str, col: usize) -> usize {
1106        let chars: Vec<char> = line.chars().collect();
1107        if chars.is_empty() {
1108            return 0;
1109        }
1110        let mut idx = col.min(chars.len());
1111        if idx == chars.len() {
1112            idx = idx.saturating_sub(1);
1113        }
1114
1115        // If current char is not a word char, check if previous was (we might be just after the word)
1116        if !Self::is_word_char(chars[idx]) {
1117            if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1118                // We are just after a word, so idx is the end (exclusive)
1119                // But wait, if we are at the space after "foo", idx points to space.
1120                // "foo " -> ' ' is at 3. word_end should be 3.
1121                // So if chars[idx] is not word char, and chars[idx-1] IS, then idx is the end.
1122                return idx;
1123            } else {
1124                // Not on a word
1125                return col.min(chars.len());
1126            }
1127        }
1128
1129        // If we are on a word char, scan forward
1130        while idx < chars.len() && Self::is_word_char(chars[idx]) {
1131            idx += 1;
1132        }
1133        idx
1134    }
1135
1136    /// Returns true when the character is part of an identifier-style word.
1137    pub(crate) fn is_word_char(ch: char) -> bool {
1138        ch == '_' || ch.is_alphanumeric()
1139    }
1140
1141    /// Computes and queues the latest LSP text change for the buffer.
1142    ///
1143    /// When auto-flush is enabled, this immediately sends changes.
1144    fn enqueue_lsp_change(&mut self) {
1145        if self.lsp_document.is_none() {
1146            return;
1147        }
1148
1149        let new_text = self.buffer.to_string();
1150        let old_text = self.lsp_shadow_text.as_str();
1151        if let Some(change) = lsp::compute_text_change(old_text, &new_text) {
1152            self.lsp_pending_changes.push(change);
1153        }
1154        self.lsp_shadow_text = new_text;
1155        if self.lsp_auto_flush {
1156            self.lsp_flush_pending_changes();
1157        }
1158    }
1159
1160    /// Refreshes search matches after buffer modification.
1161    ///
1162    /// Should be called after any operation that modifies the buffer.
1163    /// If search is active, recalculates matches and selects the one
1164    /// closest to the current cursor position.
1165    pub(crate) fn refresh_search_matches_if_needed(&mut self) {
1166        if self.search_state.is_open && !self.search_state.query.is_empty() {
1167            // Recalculate matches with current query
1168            self.search_state.update_matches(&self.buffer);
1169
1170            // Select match closest to cursor to maintain context
1171            self.search_state
1172                .select_match_near_cursor(self.cursors.primary_position());
1173        }
1174    }
1175
1176    /// Returns whether the editor has unsaved changes.
1177    ///
1178    /// # Returns
1179    ///
1180    /// `true` if there are unsaved modifications, `false` otherwise
1181    pub fn is_modified(&self) -> bool {
1182        self.history.is_modified()
1183    }
1184
1185    /// Marks the current state as saved.
1186    ///
1187    /// Call this after successfully saving the file to reset the modified state.
1188    pub fn mark_saved(&mut self) {
1189        self.history.mark_saved();
1190    }
1191
1192    /// Returns whether undo is available.
1193    pub fn can_undo(&self) -> bool {
1194        self.history.can_undo()
1195    }
1196
1197    /// Returns whether redo is available.
1198    pub fn can_redo(&self) -> bool {
1199        self.history.can_redo()
1200    }
1201
1202    /// Sets whether line wrapping is enabled.
1203    ///
1204    /// When enabled, long lines will wrap at the viewport width or at a
1205    /// configured column width.
1206    ///
1207    /// # Arguments
1208    ///
1209    /// * `enabled` - Whether to enable line wrapping
1210    ///
1211    /// # Example
1212    ///
1213    /// ```
1214    /// use iced_code_editor::CodeEditor;
1215    ///
1216    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1217    /// editor.set_wrap_enabled(false); // Disable wrapping
1218    /// ```
1219    pub fn set_wrap_enabled(&mut self, enabled: bool) {
1220        if self.wrap_enabled != enabled {
1221            self.wrap_enabled = enabled;
1222            if enabled {
1223                self.horizontal_scroll_offset = 0.0;
1224            }
1225            self.content_cache.clear();
1226            self.overlay_cache.clear();
1227        }
1228    }
1229
1230    /// Returns whether line wrapping is enabled.
1231    ///
1232    /// # Returns
1233    ///
1234    /// `true` if line wrapping is enabled, `false` otherwise
1235    pub fn wrap_enabled(&self) -> bool {
1236        self.wrap_enabled
1237    }
1238
1239    /// Enables or disables automatic indentation on Enter.
1240    ///
1241    /// When enabled, pressing Enter copies the leading whitespace of the
1242    /// current line to the new line. When disabled, the cursor is placed
1243    /// at column 0 on the new line.
1244    ///
1245    /// # Arguments
1246    ///
1247    /// * `enabled` - `true` to enable auto-indentation, `false` to disable
1248    pub fn set_auto_indent_enabled(&mut self, enabled: bool) {
1249        self.auto_indent_enabled = enabled;
1250    }
1251
1252    /// Returns whether auto-indentation is enabled.
1253    ///
1254    /// # Returns
1255    ///
1256    /// `true` if auto-indentation is enabled, `false` otherwise
1257    pub fn auto_indent_enabled(&self) -> bool {
1258        self.auto_indent_enabled
1259    }
1260
1261    /// Sets the indentation style used when pressing the Tab key.
1262    ///
1263    /// # Arguments
1264    ///
1265    /// * `style` - The indentation style (`IndentStyle::Spaces(n)` or `IndentStyle::Tab`)
1266    pub fn set_indent_style(&mut self, style: IndentStyle) {
1267        self.indent_style = style;
1268    }
1269
1270    /// Returns the current indentation style.
1271    ///
1272    /// # Returns
1273    ///
1274    /// The current [`IndentStyle`] configured for this editor
1275    pub fn indent_style(&self) -> IndentStyle {
1276        self.indent_style
1277    }
1278
1279    /// Enables or disables the search/replace functionality.
1280    ///
1281    /// When disabled, search/replace keyboard shortcuts (Ctrl+F, Ctrl+H, F3)
1282    /// will be ignored. If the search dialog is currently open, it will be closed.
1283    ///
1284    /// # Arguments
1285    ///
1286    /// * `enabled` - Whether to enable search/replace functionality
1287    ///
1288    /// # Example
1289    ///
1290    /// ```
1291    /// use iced_code_editor::CodeEditor;
1292    ///
1293    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1294    /// editor.set_search_replace_enabled(false); // Disable search/replace
1295    /// ```
1296    pub fn set_search_replace_enabled(&mut self, enabled: bool) {
1297        self.search_replace_enabled = enabled;
1298        if !enabled && self.search_state.is_open {
1299            self.search_state.close();
1300        }
1301    }
1302
1303    /// Returns whether search/replace functionality is enabled.
1304    ///
1305    /// # Returns
1306    ///
1307    /// `true` if search/replace is enabled, `false` otherwise
1308    pub fn search_replace_enabled(&self) -> bool {
1309        self.search_replace_enabled
1310    }
1311
1312    /// Sets whether LSP support is enabled.
1313    ///
1314    /// When set to `false`, any attached LSP client is detached automatically.
1315    /// Calling [`attach_lsp`] while disabled is a no-op.
1316    ///
1317    /// # Example
1318    ///
1319    /// ```
1320    /// use iced_code_editor::CodeEditor;
1321    ///
1322    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1323    /// editor.set_lsp_enabled(false);
1324    /// ```
1325    ///
1326    /// [`attach_lsp`]: CodeEditor::attach_lsp
1327    pub fn set_lsp_enabled(&mut self, enabled: bool) {
1328        self.lsp_enabled = enabled;
1329        if !enabled {
1330            self.detach_lsp();
1331        }
1332    }
1333
1334    /// Returns whether LSP support is enabled.
1335    ///
1336    /// `true` if LSP is enabled, `false` otherwise
1337    pub fn lsp_enabled(&self) -> bool {
1338        self.lsp_enabled
1339    }
1340
1341    /// Returns the syntax highlighting language identifier for this editor.
1342    ///
1343    /// This is the language key passed at construction (e.g., `"lua"`, `"rs"`, `"py"`).
1344    ///
1345    /// # Examples
1346    ///
1347    /// ```
1348    /// use iced_code_editor::CodeEditor;
1349    /// let editor = CodeEditor::new("fn main() {}", "rs");
1350    /// assert_eq!(editor.syntax(), "rs");
1351    /// ```
1352    pub fn syntax(&self) -> &str {
1353        &self.syntax
1354    }
1355
1356    /// Opens the search dialog programmatically.
1357    ///
1358    /// This is useful when wiring your own UI button instead of relying on
1359    /// keyboard shortcuts.
1360    ///
1361    /// # Returns
1362    ///
1363    /// A `Task<Message>` that focuses the search input.
1364    pub fn open_search_dialog(&mut self) -> iced::Task<Message> {
1365        self.update(&Message::OpenSearch)
1366    }
1367
1368    /// Opens the search-and-replace dialog programmatically.
1369    ///
1370    /// This is useful when wiring your own UI button instead of relying on
1371    /// keyboard shortcuts.
1372    ///
1373    /// # Returns
1374    ///
1375    /// A `Task<Message>` that focuses the search input.
1376    pub fn open_search_replace_dialog(&mut self) -> iced::Task<Message> {
1377        self.update(&Message::OpenSearchReplace)
1378    }
1379
1380    /// Closes the search dialog programmatically.
1381    ///
1382    /// # Returns
1383    ///
1384    /// A `Task<Message>` for any follow-up UI work.
1385    pub fn close_search_dialog(&mut self) -> iced::Task<Message> {
1386        self.update(&Message::CloseSearch)
1387    }
1388
1389    /// Sets the line wrapping with builder pattern.
1390    ///
1391    /// # Arguments
1392    ///
1393    /// * `enabled` - Whether to enable line wrapping
1394    ///
1395    /// # Returns
1396    ///
1397    /// Self for method chaining
1398    ///
1399    /// # Example
1400    ///
1401    /// ```
1402    /// use iced_code_editor::CodeEditor;
1403    ///
1404    /// let editor = CodeEditor::new("fn main() {}", "rs")
1405    ///     .with_wrap_enabled(false);
1406    /// ```
1407    #[must_use]
1408    pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
1409        self.wrap_enabled = enabled;
1410        self
1411    }
1412
1413    /// Sets the wrap column (fixed width wrapping).
1414    ///
1415    /// When set to `Some(n)`, lines will wrap at column `n`.
1416    /// When set to `None`, lines will wrap at the viewport width.
1417    ///
1418    /// # Arguments
1419    ///
1420    /// * `column` - The column to wrap at, or None for viewport-based wrapping
1421    ///
1422    /// # Example
1423    ///
1424    /// ```
1425    /// use iced_code_editor::CodeEditor;
1426    ///
1427    /// let editor = CodeEditor::new("fn main() {}", "rs")
1428    ///     .with_wrap_column(Some(80)); // Wrap at 80 characters
1429    /// ```
1430    #[must_use]
1431    pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
1432        self.wrap_column = column;
1433        self
1434    }
1435
1436    /// Sets whether line numbers are displayed.
1437    ///
1438    /// When disabled, the gutter is completely removed (0px width),
1439    /// providing more space for code display.
1440    ///
1441    /// # Arguments
1442    ///
1443    /// * `enabled` - Whether to display line numbers
1444    ///
1445    /// # Example
1446    ///
1447    /// ```
1448    /// use iced_code_editor::CodeEditor;
1449    ///
1450    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1451    /// editor.set_line_numbers_enabled(false); // Hide line numbers
1452    /// ```
1453    pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
1454        if self.line_numbers_enabled != enabled {
1455            self.line_numbers_enabled = enabled;
1456            self.content_cache.clear();
1457            self.overlay_cache.clear();
1458        }
1459    }
1460
1461    /// Returns whether line numbers are displayed.
1462    ///
1463    /// # Returns
1464    ///
1465    /// `true` if line numbers are displayed, `false` otherwise
1466    pub fn line_numbers_enabled(&self) -> bool {
1467        self.line_numbers_enabled
1468    }
1469
1470    /// Sets the line numbers display with builder pattern.
1471    ///
1472    /// # Arguments
1473    ///
1474    /// * `enabled` - Whether to display line numbers
1475    ///
1476    /// # Returns
1477    ///
1478    /// Self for method chaining
1479    ///
1480    /// # Example
1481    ///
1482    /// ```
1483    /// use iced_code_editor::CodeEditor;
1484    ///
1485    /// let editor = CodeEditor::new("fn main() {}", "rs")
1486    ///     .with_line_numbers_enabled(false);
1487    /// ```
1488    #[must_use]
1489    pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
1490        self.line_numbers_enabled = enabled;
1491        self
1492    }
1493
1494    /// Returns the current gutter width based on whether line numbers are enabled.
1495    ///
1496    /// # Returns
1497    ///
1498    /// `GUTTER_WIDTH` if line numbers are enabled, `0.0` otherwise
1499    pub(crate) fn gutter_width(&self) -> f32 {
1500        if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
1501    }
1502
1503    /// Removes canvas focus from this editor.
1504    ///
1505    /// This method programmatically removes focus from the canvas, preventing
1506    /// it from receiving keyboard events. The cursor will be hidden, but the
1507    /// selection will remain visible.
1508    ///
1509    /// Call this when focus should move to another widget (e.g., text input).
1510    ///
1511    /// # Example
1512    ///
1513    /// ```
1514    /// use iced_code_editor::CodeEditor;
1515    ///
1516    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1517    /// editor.lose_focus();
1518    /// ```
1519    pub fn lose_focus(&mut self) {
1520        self.has_canvas_focus = false;
1521        self.show_cursor = false;
1522        self.ime_preedit = None;
1523    }
1524
1525    /// Resets the focus lock state.
1526    ///
1527    /// This method can be called to manually unlock focus processing
1528    /// after a focus transition has completed. This is useful when
1529    /// you want to allow the editor to process input again after
1530    /// programmatic focus changes.
1531    ///
1532    /// # Example
1533    ///
1534    /// ```
1535    /// use iced_code_editor::CodeEditor;
1536    ///
1537    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1538    /// editor.reset_focus_lock();
1539    /// ```
1540    pub fn reset_focus_lock(&mut self) {
1541        self.focus_locked = false;
1542    }
1543
1544    /// Returns the screen position of the cursor.
1545    ///
1546    /// This method returns the (x, y) coordinates of the current cursor position
1547    /// relative to the editor canvas, accounting for gutter width and line height.
1548    ///
1549    /// # Returns
1550    ///
1551    /// An `Option<iced::Point>` containing the cursor position, or `None` if
1552    /// the cursor position cannot be determined.
1553    ///
1554    /// # Example
1555    ///
1556    /// ```
1557    /// use iced_code_editor::CodeEditor;
1558    ///
1559    /// let editor = CodeEditor::new("fn main() {}", "rs");
1560    /// if let Some(point) = editor.cursor_screen_position() {
1561    ///     println!("Cursor at: ({}, {})", point.x, point.y);
1562    /// }
1563    /// ```
1564    pub fn cursor_screen_position(&self) -> Option<iced::Point> {
1565        let pos = self.cursors.primary_position();
1566        self.point_from_position(pos.0, pos.1)
1567    }
1568
1569    /// Returns the current cursor position as (line, column).
1570    ///
1571    /// This method returns the logical cursor position in the buffer,
1572    /// where line and column are both 0-indexed.
1573    ///
1574    /// # Returns
1575    ///
1576    /// A tuple `(line, column)` representing the cursor position.
1577    ///
1578    /// # Example
1579    ///
1580    /// ```
1581    /// use iced_code_editor::CodeEditor;
1582    ///
1583    /// let editor = CodeEditor::new("fn main() {}", "rs");
1584    /// let (line, col) = editor.cursor_position();
1585    /// println!("Cursor at line {}, column {}", line, col);
1586    /// ```
1587    pub fn cursor_position(&self) -> (usize, usize) {
1588        self.cursors.primary_position()
1589    }
1590
1591    /// Returns the maximum content width across all lines, in pixels.
1592    ///
1593    /// Used to size the horizontal scrollbar when `wrap_enabled = false`.
1594    /// The result is cached keyed by `buffer_revision` so repeated calls are cheap.
1595    ///
1596    /// # Returns
1597    ///
1598    /// Total width in pixels including gutter, padding and a right margin.
1599    pub(crate) fn max_content_width(&self) -> f32 {
1600        let mut cache = self.max_content_width_cache.borrow_mut();
1601        if let Some((rev, w)) = *cache
1602            && rev == self.buffer_revision
1603        {
1604            return w;
1605        }
1606
1607        let gutter = self.gutter_width();
1608        let max_line_width = (0..self.buffer.line_count())
1609            .map(|i| {
1610                measure_text_width(
1611                    self.buffer.line(i),
1612                    self.full_char_width,
1613                    self.char_width,
1614                )
1615            })
1616            .fold(0.0_f32, f32::max);
1617
1618        // gutter + left padding + text + right margin
1619        let total = gutter + 5.0 + max_line_width + 20.0;
1620        *cache = Some((self.buffer_revision, total));
1621        total
1622    }
1623
1624    /// Returns wrapped "visual lines" for the current buffer and layout, with memoization.
1625    ///
1626    /// The editor frequently needs the wrapped view of the buffer:
1627    /// - hit-testing (mouse selection, cursor placement)
1628    /// - mapping logical ↔ visual positions
1629    /// - rendering (text, line numbers, highlights)
1630    ///
1631    /// Computing visual lines is relatively expensive for large files, so we
1632    /// cache the result keyed by:
1633    /// - `buffer_revision` (buffer content changes)
1634    /// - viewport width / gutter width (layout changes)
1635    /// - wrapping settings (wrap enabled / wrap column)
1636    /// - measured character widths (font / size changes)
1637    ///
1638    /// The returned `Rc<Vec<VisualLine>>` is cheap to clone and allows multiple
1639    /// rendering passes (content + overlay layers) to share the same computed
1640    /// layout without extra allocation.
1641    pub(crate) fn visual_lines_cached(
1642        &self,
1643        viewport_width: f32,
1644    ) -> Rc<Vec<wrapping::VisualLine>> {
1645        let key = VisualLinesKey {
1646            buffer_revision: self.buffer_revision,
1647            viewport_width_bits: viewport_width.to_bits(),
1648            gutter_width_bits: self.gutter_width().to_bits(),
1649            wrap_enabled: self.wrap_enabled,
1650            wrap_column: self.wrap_column,
1651            full_char_width_bits: self.full_char_width.to_bits(),
1652            char_width_bits: self.char_width.to_bits(),
1653        };
1654
1655        let mut cache = self.visual_lines_cache.borrow_mut();
1656        if let Some(existing) = cache.as_ref()
1657            && existing.key == key
1658        {
1659            return existing.visual_lines.clone();
1660        }
1661
1662        let wrapping_calc = wrapping::WrappingCalculator::new(
1663            self.wrap_enabled,
1664            self.wrap_column,
1665            self.full_char_width,
1666            self.char_width,
1667        );
1668        let visual_lines = wrapping_calc.calculate_visual_lines(
1669            &self.buffer,
1670            viewport_width,
1671            self.gutter_width(),
1672        );
1673        let visual_lines = Rc::new(visual_lines);
1674
1675        *cache =
1676            Some(VisualLinesCache { key, visual_lines: visual_lines.clone() });
1677        visual_lines
1678    }
1679
1680    /// Initiates a "Go to Definition" request for the symbol at the current cursor position.
1681    ///
1682    /// This method converts the current cursor coordinates into an LSP-compatible position
1683    /// and delegates the request to the active `LspClient`, if one is attached.
1684    pub fn lsp_request_definition(&mut self) {
1685        let position = self.lsp_position_from_cursor();
1686        if let (Some(client), Some(document)) =
1687            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1688        {
1689            client.request_definition(document, position);
1690        }
1691    }
1692
1693    /// Initiates a "Go to Definition" request for the symbol at the specified screen coordinates.
1694    ///
1695    /// This is typically used for mouse interactions (e.g., Ctrl+Click). It first resolves
1696    /// the screen coordinates to a text position and then sends the request.
1697    ///
1698    /// # Returns
1699    ///
1700    /// `true` if the request was successfully sent (i.e., a valid position was found and an LSP client is active),
1701    /// `false` otherwise.
1702    pub fn lsp_request_definition_at(&mut self, point: iced::Point) -> bool {
1703        let Some(position) = self.lsp_position_from_point(point) else {
1704            return false;
1705        };
1706        if let (Some(client), Some(document)) =
1707            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1708        {
1709            client.request_definition(document, position);
1710            return true;
1711        }
1712        false
1713    }
1714}
1715
1716#[cfg(test)]
1717mod tests {
1718    use super::*;
1719    use std::cell::RefCell;
1720    use std::rc::Rc;
1721
1722    #[test]
1723    fn test_compare_floats() {
1724        // Equal cases
1725        assert_eq!(
1726            compare_floats(1.0, 1.0),
1727            CmpOrdering::Equal,
1728            "Exact equality"
1729        );
1730        assert_eq!(
1731            compare_floats(1.0, 1.0 + 0.0001),
1732            CmpOrdering::Equal,
1733            "Within epsilon (positive)"
1734        );
1735        assert_eq!(
1736            compare_floats(1.0, 1.0 - 0.0001),
1737            CmpOrdering::Equal,
1738            "Within epsilon (negative)"
1739        );
1740
1741        // Greater cases
1742        assert_eq!(
1743            compare_floats(1.0 + 0.002, 1.0),
1744            CmpOrdering::Greater,
1745            "Definitely greater"
1746        );
1747        assert_eq!(
1748            compare_floats(1.0011, 1.0),
1749            CmpOrdering::Greater,
1750            "Just above epsilon"
1751        );
1752
1753        // Less cases
1754        assert_eq!(
1755            compare_floats(1.0, 1.0 + 0.002),
1756            CmpOrdering::Less,
1757            "Definitely less"
1758        );
1759        assert_eq!(
1760            compare_floats(1.0, 1.0011),
1761            CmpOrdering::Less,
1762            "Just below negative epsilon"
1763        );
1764    }
1765
1766    #[test]
1767    fn test_measure_text_width_ascii() {
1768        // "abc" (3 chars) -> 3 * CHAR_WIDTH
1769        let text = "abc";
1770        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1771        let expected = CHAR_WIDTH * 3.0;
1772        assert_eq!(
1773            compare_floats(width, expected),
1774            CmpOrdering::Equal,
1775            "Width mismatch for ASCII"
1776        );
1777    }
1778
1779    #[test]
1780    fn test_measure_text_width_cjk() {
1781        // "你好" (2 chars) -> 2 * FONT_SIZE
1782        // Chinese characters are typically full-width.
1783        // width = 2 * FONT_SIZE
1784        let text = "你好";
1785        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1786        let expected = FONT_SIZE * 2.0;
1787        assert_eq!(
1788            compare_floats(width, expected),
1789            CmpOrdering::Equal,
1790            "Width mismatch for CJK"
1791        );
1792    }
1793
1794    #[test]
1795    fn test_measure_text_width_mixed() {
1796        // "Hi" (2 chars) -> 2 * CHAR_WIDTH
1797        // "你好" (2 chars) -> 2 * FONT_SIZE
1798        let text = "Hi你好";
1799        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1800        let expected = CHAR_WIDTH * 2.0 + FONT_SIZE * 2.0;
1801        assert_eq!(
1802            compare_floats(width, expected),
1803            CmpOrdering::Equal,
1804            "Width mismatch for mixed content"
1805        );
1806    }
1807
1808    #[test]
1809    fn test_measure_text_width_control_chars() {
1810        // "\t\n" (2 chars)
1811        // width = 4 * CHAR_WIDTH (tab) + 0 (newline)
1812        let text = "\t\n";
1813        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1814        let expected = CHAR_WIDTH * TAB_WIDTH as f32;
1815        assert_eq!(
1816            compare_floats(width, expected),
1817            CmpOrdering::Equal,
1818            "Width mismatch for control chars"
1819        );
1820    }
1821
1822    #[test]
1823    fn test_measure_text_width_empty() {
1824        let text = "";
1825        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1826        assert!(
1827            (width - 0.0).abs() < f32::EPSILON,
1828            "Width should be 0 for empty string"
1829        );
1830    }
1831
1832    #[test]
1833    fn test_measure_text_width_emoji() {
1834        // "👋" (1 char, width > 1) -> FONT_SIZE
1835        let text = "👋";
1836        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1837        let expected = FONT_SIZE;
1838        assert_eq!(
1839            compare_floats(width, expected),
1840            CmpOrdering::Equal,
1841            "Width mismatch for emoji"
1842        );
1843    }
1844
1845    #[test]
1846    fn test_measure_text_width_korean() {
1847        // "안녕하세요" (5 chars)
1848        // Korean characters are typically full-width.
1849        // width = 5 * FONT_SIZE
1850        let text = "안녕하세요";
1851        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1852        let expected = FONT_SIZE * 5.0;
1853        assert_eq!(
1854            compare_floats(width, expected),
1855            CmpOrdering::Equal,
1856            "Width mismatch for Korean"
1857        );
1858    }
1859
1860    #[test]
1861    fn test_measure_text_width_japanese() {
1862        // "こんにちは" (Hiragana, 5 chars) -> 5 * FONT_SIZE
1863        // "カタカナ" (Katakana, 4 chars) -> 4 * FONT_SIZE
1864        // "漢字" (Kanji, 2 chars) -> 2 * FONT_SIZE
1865
1866        let text_hiragana = "こんにちは";
1867        let width_hiragana =
1868            measure_text_width(text_hiragana, FONT_SIZE, CHAR_WIDTH);
1869        let expected_hiragana = FONT_SIZE * 5.0;
1870        assert_eq!(
1871            compare_floats(width_hiragana, expected_hiragana),
1872            CmpOrdering::Equal,
1873            "Width mismatch for Hiragana"
1874        );
1875
1876        let text_katakana = "カタカナ";
1877        let width_katakana =
1878            measure_text_width(text_katakana, FONT_SIZE, CHAR_WIDTH);
1879        let expected_katakana = FONT_SIZE * 4.0;
1880        assert_eq!(
1881            compare_floats(width_katakana, expected_katakana),
1882            CmpOrdering::Equal,
1883            "Width mismatch for Katakana"
1884        );
1885
1886        let text_kanji = "漢字";
1887        let width_kanji = measure_text_width(text_kanji, FONT_SIZE, CHAR_WIDTH);
1888        let expected_kanji = FONT_SIZE * 2.0;
1889        assert_eq!(
1890            compare_floats(width_kanji, expected_kanji),
1891            CmpOrdering::Equal,
1892            "Width mismatch for Kanji"
1893        );
1894    }
1895
1896    #[test]
1897    fn test_set_font_size() {
1898        let mut editor = CodeEditor::new("", "rs");
1899
1900        // Initial state (defaults)
1901        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
1902        assert!((editor.line_height() - 20.0).abs() < f32::EPSILON);
1903
1904        // Test auto adjust = true
1905        editor.set_font_size(28.0, true);
1906        assert!((editor.font_size() - 28.0).abs() < f32::EPSILON);
1907        // Line height should double: 20.0 * (28.0/14.0) = 40.0
1908        assert_eq!(
1909            compare_floats(editor.line_height(), 40.0),
1910            CmpOrdering::Equal
1911        );
1912
1913        // Test auto adjust = false
1914        // First set line height to something custom
1915        editor.set_line_height(50.0);
1916        // Change font size but keep line height
1917        editor.set_font_size(14.0, false);
1918        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
1919        // Line height should stay 50.0
1920        assert_eq!(
1921            compare_floats(editor.line_height(), 50.0),
1922            CmpOrdering::Equal
1923        );
1924        // Char width should have scaled back to roughly default (but depends on measurement)
1925        // We check if it is close to the expected value, but since measurement can vary,
1926        // we just ensure it is positive and close to what we expect (around 8.4)
1927        assert!(editor.char_width > 0.0);
1928        assert!((editor.char_width - CHAR_WIDTH).abs() < 0.5);
1929    }
1930
1931    #[test]
1932    fn test_measure_single_char_width() {
1933        let editor = CodeEditor::new("", "rs");
1934
1935        // Measure 'a'
1936        let width_a = editor.measure_single_char_width("a");
1937        assert!(width_a > 0.0, "Width of 'a' should be positive");
1938
1939        // Measure Chinese char
1940        let width_cjk = editor.measure_single_char_width("汉");
1941        assert!(width_cjk > 0.0, "Width of '汉' should be positive");
1942
1943        assert!(
1944            width_cjk > width_a,
1945            "Width of '汉' should be greater than 'a'"
1946        );
1947
1948        // Check that width_cjk is roughly double of width_a (common in terminal fonts)
1949        // but we just check it is significantly larger
1950        assert!(width_cjk >= width_a * 1.5);
1951    }
1952
1953    #[test]
1954    fn test_set_line_height() {
1955        let mut editor = CodeEditor::new("", "rs");
1956
1957        // Initial state
1958        assert!((editor.line_height() - LINE_HEIGHT).abs() < f32::EPSILON);
1959
1960        // Set custom line height
1961        editor.set_line_height(35.0);
1962        assert!((editor.line_height() - 35.0).abs() < f32::EPSILON);
1963
1964        // Font size should remain unchanged
1965        assert!((editor.font_size() - FONT_SIZE).abs() < f32::EPSILON);
1966    }
1967
1968    #[test]
1969    fn test_visual_lines_cached_reuses_cache_for_same_key() {
1970        let editor = CodeEditor::new("a\nb\nc", "rs");
1971
1972        let first = editor.visual_lines_cached(800.0);
1973        let second = editor.visual_lines_cached(800.0);
1974
1975        assert!(
1976            Rc::ptr_eq(&first, &second),
1977            "visual_lines_cached should reuse the cached Rc for identical keys"
1978        );
1979    }
1980
1981    #[derive(Default)]
1982    struct TestLspClient {
1983        changes: Rc<RefCell<Vec<Vec<lsp::LspTextChange>>>>,
1984    }
1985
1986    impl lsp::LspClient for TestLspClient {
1987        fn did_change(
1988            &mut self,
1989            _document: &lsp::LspDocument,
1990            changes: &[lsp::LspTextChange],
1991        ) {
1992            self.changes.borrow_mut().push(changes.to_vec());
1993        }
1994    }
1995
1996    #[test]
1997    fn test_word_start_in_line() {
1998        let line = "foo_bar baz";
1999        assert_eq!(CodeEditor::word_start_in_line(line, 0), 0);
2000        assert_eq!(CodeEditor::word_start_in_line(line, 2), 0);
2001        assert_eq!(CodeEditor::word_start_in_line(line, 4), 0);
2002        assert_eq!(CodeEditor::word_start_in_line(line, 7), 0);
2003        assert_eq!(CodeEditor::word_start_in_line(line, 9), 8);
2004    }
2005
2006    #[test]
2007    fn test_enqueue_lsp_change_auto_flush() {
2008        let changes = Rc::new(RefCell::new(Vec::new()));
2009        let client = TestLspClient { changes: Rc::clone(&changes) };
2010        let mut editor = CodeEditor::new("hello", "rs");
2011        editor.attach_lsp(
2012            Box::new(client),
2013            lsp::LspDocument::new("file:///test.rs", "rust"),
2014        );
2015        editor.set_lsp_auto_flush(true);
2016
2017        editor.buffer.insert_char(0, 5, '!');
2018        editor.enqueue_lsp_change();
2019
2020        let changes = changes.borrow();
2021        assert_eq!(changes.len(), 1);
2022        assert_eq!(changes[0].len(), 1);
2023        let change = &changes[0][0];
2024        assert_eq!(change.text, "!");
2025        assert_eq!(change.range.start.line, 0);
2026        assert_eq!(change.range.start.character, 5);
2027        assert_eq!(change.range.end.line, 0);
2028        assert_eq!(change.range.end.character, 5);
2029    }
2030
2031    #[test]
2032    fn test_visual_lines_cached_changes_on_viewport_width_change() {
2033        let editor = CodeEditor::new("a\nb\nc", "rs");
2034
2035        let first = editor.visual_lines_cached(800.0);
2036        let second = editor.visual_lines_cached(801.0);
2037
2038        assert!(
2039            !Rc::ptr_eq(&first, &second),
2040            "visual_lines_cached should recompute when viewport width changes"
2041        );
2042    }
2043
2044    #[test]
2045    fn test_visual_lines_cached_changes_on_buffer_revision_change() {
2046        let mut editor = CodeEditor::new("a\nb\nc", "rs");
2047
2048        let first = editor.visual_lines_cached(800.0);
2049        editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
2050        let second = editor.visual_lines_cached(800.0);
2051
2052        assert!(
2053            !Rc::ptr_eq(&first, &second),
2054            "visual_lines_cached should recompute when buffer_revision changes"
2055        );
2056    }
2057
2058    #[test]
2059    fn test_max_content_width_increases_with_longer_lines() {
2060        let short = CodeEditor::new("ab", "rs");
2061        let long =
2062            CodeEditor::new("abcdefghijklmnopqrstuvwxyz0123456789", "rs");
2063
2064        assert!(
2065            long.max_content_width() > short.max_content_width(),
2066            "Longer lines should produce a greater max_content_width"
2067        );
2068    }
2069
2070    #[test]
2071    fn test_max_content_width_cached_by_revision() {
2072        let mut editor = CodeEditor::new("hello", "rs");
2073        let w1 = editor.max_content_width();
2074
2075        // Same revision → cache hit
2076        let w2 = editor.max_content_width();
2077        assert!(
2078            (w1 - w2).abs() < f32::EPSILON,
2079            "Repeated calls with same revision should return identical value"
2080        );
2081
2082        // Bump revision to simulate edit
2083        editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
2084        // Update the buffer to reflect a longer line
2085        editor.buffer = crate::text_buffer::TextBuffer::new(
2086            "hello world with extra content",
2087        );
2088        let w3 = editor.max_content_width();
2089        assert!(
2090            w3 > w1,
2091            "After revision bump with longer content, width should increase"
2092        );
2093    }
2094
2095    #[test]
2096    fn test_syntax_getter() {
2097        let editor = CodeEditor::new("", "lua");
2098        assert_eq!(editor.syntax(), "lua");
2099    }
2100}