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