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