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::{BTreeMap, 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 context_menu;
43mod cursor;
44pub(crate) mod cursor_set;
45pub mod folding;
46mod goto_line;
47mod goto_line_dialog;
48pub mod history;
49pub mod ime_requester;
50pub mod lsp;
51#[cfg(all(feature = "lsp-process", not(target_arch = "wasm32")))]
52pub mod lsp_process;
53mod search;
54mod search_dialog;
55mod selection;
56mod update;
57mod view;
58mod vim;
59mod wrapping;
60
61pub use context_menu::{ContextMenuEntry, ContextMenuItem};
62pub use vim::VimMode;
63
64/// Hidden re-exports for the benchmark harness in `benches/`.
65///
66/// This module is compiled only with the `bench` feature and is **not** part
67/// of the public API. It exposes internal hot-path functions so the
68/// `criterion` benchmarks (which run as a separate crate) can measure them.
69#[doc(hidden)]
70#[cfg(feature = "bench")]
71pub mod bench_support {
72    pub use super::canvas_impl::highlight_line_spans;
73    pub use super::folding::compute_foldable_regions;
74    pub use super::search::find_matches;
75    pub use super::wrapping::WrappingCalculator;
76    pub use crate::text_buffer::TextBuffer;
77
78    /// Stateful harness for measuring the normal localized typing path.
79    pub struct IncrementalEditBenchmark {
80        editor: super::CodeEditor,
81    }
82
83    impl IncrementalEditBenchmark {
84        /// Creates an editor, primes its visual-line cache, and places the
85        /// cursor at `line`/`column`.
86        pub fn new(content: &str, line: usize, column: usize) -> Self {
87            let mut editor = super::CodeEditor::new(content, "rs")
88                .with_wrap_column(Some(80));
89            editor.request_focus();
90            editor.has_canvas_focus = true;
91            editor.focus_locked = false;
92            editor.cursors.primary_mut().position = (line, column);
93            let _ = editor.visual_lines_cached(800.0);
94            Self { editor }
95        }
96
97        /// Inserts and removes one character, leaving content size stable for
98        /// repeated Criterion iterations.
99        pub fn insert_and_backspace(&mut self) -> u64 {
100            let _ = self.editor.update(&super::Message::CharacterInput('x'));
101            let _ = self.editor.update(&super::Message::Backspace);
102            if self.editor.is_grouping {
103                self.editor.history.end_group();
104                self.editor.is_grouping = false;
105            }
106            self.editor.buffer_revision
107        }
108    }
109
110    struct NoopLspClient;
111
112    impl super::lsp::LspClient for NoopLspClient {}
113
114    /// Stateful harness for the incremental LSP synchronization path.
115    pub struct IncrementalLspEditBenchmark {
116        editor: super::CodeEditor,
117    }
118
119    impl IncrementalLspEditBenchmark {
120        /// Creates and primes a focused editor with a no-op LSP client.
121        pub fn new(content: &str, line: usize, column: usize) -> Self {
122            let mut editor = super::CodeEditor::new(content, "rs")
123                .with_wrap_column(Some(80));
124            editor.attach_lsp(
125                Box::new(NoopLspClient),
126                super::lsp::LspDocument::new("file:///benchmark.rs", "rust"),
127            );
128            editor.request_focus();
129            editor.has_canvas_focus = true;
130            editor.focus_locked = false;
131            editor.cursors.primary_mut().position = (line, column);
132            let _ = editor.visual_lines_cached(800.0);
133            Self { editor }
134        }
135
136        /// Inserts and removes one character while sending incremental LSP
137        /// changes for both edits.
138        pub fn insert_and_backspace(&mut self) -> u64 {
139            let _ = self.editor.update(&super::Message::CharacterInput('x'));
140            let _ = self.editor.update(&super::Message::Backspace);
141            if self.editor.is_grouping {
142                self.editor.history.end_group();
143                self.editor.is_grouping = false;
144            }
145            self.editor.buffer_revision
146        }
147    }
148
149    /// Stateful harness for typing with wrapping disabled.
150    pub struct IncrementalNoWrapEditBenchmark {
151        editor: super::CodeEditor,
152    }
153
154    impl IncrementalNoWrapEditBenchmark {
155        /// Creates an editor and primes both layout and horizontal-width caches.
156        pub fn new(content: &str, line: usize, column: usize) -> Self {
157            let mut editor = super::CodeEditor::new(content, "rs");
158            editor.set_wrap_enabled(false);
159            editor.request_focus();
160            editor.has_canvas_focus = true;
161            editor.focus_locked = false;
162            editor.cursors.primary_mut().position = (line, column);
163            let _ = editor.visual_lines_cached(800.0);
164            let _ = editor.max_content_width();
165            Self { editor }
166        }
167
168        /// Inserts and removes one character without triggering a whole-file
169        /// maximum-width scan.
170        pub fn insert_and_backspace(&mut self) -> u64 {
171            let _ = self.editor.update(&super::Message::CharacterInput('x'));
172            let _ = self.editor.update(&super::Message::Backspace);
173            if self.editor.is_grouping {
174                self.editor.history.end_group();
175                self.editor.is_grouping = false;
176            }
177            self.editor.buffer_revision
178        }
179    }
180
181    /// Stateful harness for typing while a large-file search is open.
182    pub struct IncrementalSearchEditBenchmark {
183        editor: super::CodeEditor,
184    }
185
186    impl IncrementalSearchEditBenchmark {
187        /// Creates an editor with populated search results and a warm layout.
188        pub fn new(
189            content: &str,
190            query: &str,
191            line: usize,
192            column: usize,
193        ) -> Self {
194            let mut editor = super::CodeEditor::new(content, "rs")
195                .with_wrap_column(Some(80));
196            editor.search_state.open_search();
197            editor.search_state.set_query(query.to_owned(), &editor.buffer);
198            editor.request_focus();
199            editor.has_canvas_focus = true;
200            editor.focus_locked = false;
201            editor.cursors.primary_mut().position = (line, column);
202            let _ = editor.visual_lines_cached(800.0);
203            Self { editor }
204        }
205
206        /// Inserts and removes one character while maintaining search matches.
207        pub fn insert_and_backspace(&mut self) -> u64 {
208            let _ = self.editor.update(&super::Message::CharacterInput('x'));
209            let _ = self.editor.update(&super::Message::Backspace);
210            if self.editor.is_grouping {
211                self.editor.history.end_group();
212                self.editor.is_grouping = false;
213            }
214            self.editor.buffer_revision
215        }
216    }
217
218    /// Measures the incremental wrapping path without exposing internal visual
219    /// line types as public editor API.
220    pub fn calculate_visual_line_range_len(
221        calculator: &WrappingCalculator,
222        buffer: &TextBuffer,
223        viewport_width: f32,
224        gutter_width: f32,
225        start_line: usize,
226        end_line: usize,
227    ) -> usize {
228        calculator
229            .calculate_visual_lines_range(
230                buffer,
231                viewport_width,
232                gutter_width,
233                &std::collections::HashSet::new(),
234                start_line..end_line,
235            )
236            .len()
237    }
238}
239
240/// Canvas-based text editor constants
241pub(crate) const FONT_SIZE: f32 = 14.0;
242pub(crate) const LINE_HEIGHT: f32 = 20.0;
243pub(crate) const CHAR_WIDTH: f32 = 8.4; // Monospace character width
244pub(crate) const TAB_WIDTH: usize = 4;
245pub(crate) const GUTTER_WIDTH: f32 = 45.0;
246/// Width in pixels of the fold margin (chevron column) added to the gutter when
247/// code folding is enabled.
248pub(crate) const FOLD_MARGIN_WIDTH: f32 = 14.0;
249pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
250    std::time::Duration::from_millis(530);
251
252/// Measures the width of a single character.
253///
254/// # Arguments
255///
256/// * `c` - The character to measure
257/// * `full_char_width` - The width of a full-width character
258/// * `char_width` - The width of the character
259///
260/// # Returns
261///
262/// The calculated width of the character as a `f32`
263pub(crate) fn measure_char_width(
264    c: char,
265    full_char_width: f32,
266    char_width: f32,
267) -> f32 {
268    if c == '\t' {
269        return char_width * TAB_WIDTH as f32;
270    }
271    match c.width() {
272        Some(w) if w > 1 => full_char_width,
273        Some(_) => char_width,
274        None => 0.0,
275    }
276}
277
278/// Measures rendered text width, accounting for CJK wide characters.
279///
280/// - Wide characters (e.g. Chinese) use FONT_SIZE.
281/// - Narrow characters (e.g. Latin) use CHAR_WIDTH.
282/// - Control characters (except tab) have width 0.
283///
284/// # Arguments
285///
286/// * `text` - The text string to measure
287/// * `full_char_width` - The width of a full-width character
288/// * `char_width` - The width of a regular character
289///
290/// # Returns
291///
292/// The total calculated width of the text as a `f32`
293pub(crate) fn measure_text_width(
294    text: &str,
295    full_char_width: f32,
296    char_width: f32,
297) -> f32 {
298    text.chars()
299        .map(|c| measure_char_width(c, full_char_width, char_width))
300        .sum()
301}
302
303/// Epsilon value for floating-point comparisons in text layout.
304pub(crate) const EPSILON: f32 = 0.001;
305/// Multiplier used to extend the cached render window beyond the visible range.
306/// The cache window margin is computed as:
307///     margin = visible_lines_count * CACHE_WINDOW_MARGIN_MULTIPLIER
308/// A larger margin reduces how often we clear and rebuild the canvas cache when
309/// scrolling, improving performance on very large files while still ensuring
310/// correct initial rendering during the first scroll.
311pub(crate) const CACHE_WINDOW_MARGIN_MULTIPLIER: usize = 2;
312/// Maximum number of previously unseen logical lines syntect may parse while
313/// rebuilding one content frame.
314///
315/// Syntax state is sequential, so jumping far into a large file can otherwise
316/// parse every line from the start in one blocking draw call. Lines beyond this
317/// budget temporarily use the editor's plain text color and will be highlighted
318/// as later content redraws advance the cached parser state.
319pub(crate) const HIGHLIGHT_LINES_PER_FRAME: usize = 2_000;
320
321/// Compares two floating point numbers with a small epsilon tolerance.
322///
323/// # Arguments
324///
325/// * `a` - first float number
326/// * `b` - second float number
327///
328/// # Returns
329///
330/// * `Ordering::Equal` if `abs(a - b) < EPSILON`
331/// * `Ordering::Greater` if `a > b` (and not equal)
332/// * `Ordering::Less` if `a < b` (and not equal)
333pub(crate) fn compare_floats(a: f32, b: f32) -> CmpOrdering {
334    if (a - b).abs() < EPSILON {
335        CmpOrdering::Equal
336    } else if a > b {
337        CmpOrdering::Greater
338    } else {
339        CmpOrdering::Less
340    }
341}
342
343#[derive(Debug, Clone)]
344pub(crate) struct ImePreedit {
345    pub(crate) content: String,
346    pub(crate) selection: Option<Range<usize>>,
347}
348
349/// Conservative logical-line range captured immediately before an edit.
350///
351/// It lets LSP synchronization send one incremental range replacement without
352/// serializing and diffing the entire document after every keystroke.
353pub(crate) struct LspEditSnapshot {
354    pub(crate) start_line: usize,
355    pub(crate) old_end_exclusive: usize,
356    pub(crate) old_line_count: usize,
357    pub(crate) old_end: lsp::LspPosition,
358}
359
360/// Canvas-based high-performance text editor.
361pub struct CodeEditor {
362    /// Unique ID for this editor instance (for focus management)
363    pub(crate) editor_id: u64,
364    /// Text buffer
365    pub(crate) buffer: TextBuffer,
366    /// All cursor positions (multi-cursor support).
367    pub(crate) cursors: cursor_set::CursorSet,
368    /// Horizontal scroll offset in pixels, only used when wrap_enabled = false
369    pub(crate) horizontal_scroll_offset: f32,
370    /// Editor theme style
371    pub(crate) style: Style,
372    /// Syntax highlighting language
373    pub(crate) syntax: String,
374    /// Last cursor blink time
375    pub(crate) last_blink: Instant,
376    /// Cursor visible state
377    pub(crate) cursor_visible: bool,
378    /// Mouse is currently dragging for selection
379    pub(crate) is_dragging: bool,
380    /// Cached geometry for the "content" layer.
381    ///
382    /// This layer includes expensive-to-build, mostly static visuals such as:
383    /// - syntax-highlighted text glyphs
384    /// - line numbers / gutter text
385    ///
386    /// It is intentionally kept stable across selection/cursor movement so
387    /// that mouse-drag selection feels smooth.
388    pub(crate) content_cache: canvas::Cache,
389    /// Cached geometry for the "overlay" layer.
390    ///
391    /// This layer includes visuals that change frequently without modifying the
392    /// underlying buffer, such as:
393    /// - cursor and current-line highlight
394    /// - selection highlight
395    /// - search match highlights
396    /// - IME preedit decorations
397    ///
398    /// Keeping overlays in a separate cache avoids invalidating the content
399    /// layer on every cursor blink or selection drag.
400    pub(crate) overlay_cache: canvas::Cache,
401    /// Scrollable ID for programmatic scrolling
402    pub(crate) scrollable_id: Id,
403    /// ID for the horizontal scrollable widget (only used when wrap_enabled = false)
404    pub(crate) horizontal_scrollable_id: Id,
405    /// Incremental per-line width index for the horizontal scrollbar.
406    pub(crate) max_content_width_cache: RefCell<Option<MaxContentWidthCache>>,
407    /// Current viewport scroll position (Y offset)
408    pub(crate) viewport_scroll: f32,
409    /// Viewport height (visible area)
410    pub(crate) viewport_height: f32,
411    /// Viewport width (visible area)
412    pub(crate) viewport_width: f32,
413    /// Command history for undo/redo
414    pub(crate) history: CommandHistory,
415    /// Whether we're currently grouping commands (for smart undo)
416    pub(crate) is_grouping: bool,
417    /// Line wrapping enabled
418    pub(crate) wrap_enabled: bool,
419    /// Auto-indentation enabled
420    pub(crate) auto_indent_enabled: bool,
421    /// Indentation style (spaces or tab)
422    pub(crate) indent_style: IndentStyle,
423    /// Wrap column (None = wrap at viewport width)
424    pub(crate) wrap_column: Option<usize>,
425    /// Whether code folding (collapse/expand blocks) is enabled.
426    pub(crate) folding_enabled: bool,
427    /// Header line indices of regions that are currently collapsed.
428    pub(crate) collapsed_folds: HashSet<usize>,
429    /// Monotonic revision counter for fold state.
430    ///
431    /// Bumped whenever the collapsed set or the folding toggle changes, so that
432    /// derived layout caches (visual lines) are invalidated.
433    pub(crate) fold_revision: u64,
434    /// Cached foldable regions, keyed by `buffer_revision`.
435    pub(crate) foldable_regions_cache:
436        RefCell<Option<(u64, Rc<Vec<folding::FoldRegion>>)>>,
437    /// Search state
438    pub(crate) search_state: search::SearchState,
439    /// Custom entries displayed before the built-in context-menu actions.
440    custom_context_menu_entries: Vec<ContextMenuEntry>,
441    /// Whether the built-in editing actions are shown in the context menu.
442    default_context_menu_enabled: bool,
443    /// Whether the built-in reveal-in-file-manager action is shown.
444    reveal_in_file_manager_enabled: bool,
445    /// Go-to-line dialog state
446    pub(crate) goto_line_state: goto_line::GotoLineState,
447    /// Whether Vim key handling is enabled for this editor instance.
448    vim_enabled: bool,
449    /// Per-editor Vim mode, parser prefixes and unnamed register.
450    pub(crate) vim_state: vim::VimState,
451    /// Translations for UI text
452    pub(crate) translations: Translations,
453    /// Whether search/replace functionality is enabled
454    pub(crate) search_replace_enabled: bool,
455    /// Whether line numbers are displayed
456    pub(crate) line_numbers_enabled: bool,
457    /// Whether to render whitespace characters visibly (spaces as `·`, tabs as `→`)
458    pub(crate) show_whitespace: bool,
459    /// Whether LSP support is enabled
460    pub(crate) lsp_enabled: bool,
461    /// Active LSP client connection, if configured.
462    pub(crate) lsp_client: Option<Box<dyn lsp::LspClient>>,
463    /// Metadata for the currently open LSP document.
464    pub(crate) lsp_document: Option<lsp::LspDocument>,
465    /// Pending incremental LSP text changes not yet flushed.
466    pub(crate) lsp_pending_changes: Vec<lsp::LspTextChange>,
467    /// Shadow copy of buffer content used to compute LSP deltas.
468    pub(crate) lsp_shadow_text: String,
469    /// Whether `lsp_shadow_text` still exactly matches the server document.
470    pub(crate) lsp_shadow_is_current: bool,
471    /// Current server-side line count, maintained incrementally.
472    pub(crate) lsp_synced_line_count: usize,
473    /// Length of the current server-side final line in Unicode scalar values.
474    pub(crate) lsp_synced_last_line_len: usize,
475    /// Pre-edit range used to build a bounded incremental LSP change.
476    pub(crate) lsp_edit_snapshot: Option<LspEditSnapshot>,
477    /// Whether to auto-flush LSP changes after edits.
478    pub(crate) lsp_auto_flush: bool,
479    /// Whether the canvas has user input focus (for keyboard events)
480    pub(crate) has_canvas_focus: bool,
481    /// Whether input processing is locked to prevent focus stealing
482    pub(crate) focus_locked: bool,
483    /// Whether to show the cursor (for rendering)
484    pub(crate) show_cursor: bool,
485    /// Current keyboard modifiers state (Ctrl, Alt, Shift, Logo).
486    ///
487    /// This is updated via subscription events and used to handle modifier-dependent
488    /// interactions, such as "Ctrl+Click" for jumping to a definition.
489    pub(crate) modifiers: Cell<iced::keyboard::Modifiers>,
490    /// Last left-button press (time, position, consecutive count), used to
491    /// detect double/triple clicks.
492    pub(crate) last_click: Cell<Option<(Instant, iced::Point, u8)>>,
493    /// The font used for rendering text
494    pub(crate) font: iced::Font,
495    /// IME pre-edit state (for CJK input)
496    pub(crate) ime_preedit: Option<ImePreedit>,
497    /// Font size in pixels
498    pub(crate) font_size: f32,
499    /// Full character width (wide chars like CJK) in pixels
500    pub(crate) full_char_width: f32,
501    /// Line height in pixels
502    pub(crate) line_height: f32,
503    /// Character width in pixels
504    pub(crate) char_width: f32,
505    /// Cached render window: the first visual line index included in the cache.
506    /// We keep a larger window than the currently visible range to avoid clearing
507    /// the canvas cache on every small scroll. Only when scrolling crosses the
508    /// window boundary do we re-window and clear the cache.
509    pub(crate) last_first_visible_line: usize,
510    /// Cached render window start line (inclusive)
511    pub(crate) cache_window_start_line: usize,
512    /// Cached render window end line (exclusive)
513    pub(crate) cache_window_end_line: usize,
514    /// Monotonic revision counter for buffer content.
515    ///
516    /// Any operation that changes the buffer must bump this counter to
517    /// invalidate derived layout caches (e.g. wrapping / visual lines). The
518    /// exact value is not semantically meaningful, so `wrapping_add` is used to
519    /// avoid overflow panics while still producing a different key.
520    pub(crate) buffer_revision: u64,
521    /// Cached result of line wrapping ("visual lines") for the current layout key.
522    ///
523    /// This is stored behind a `RefCell` because wrapping is needed during
524    /// rendering (where we only have `&self`), but we still want to memoize the
525    /// expensive computation without forcing external mutability.
526    visual_lines_cache: RefCell<Option<VisualLinesCache>>,
527    /// Sequential per-line syntax-highlight cache (see [`HighlightCache`]).
528    ///
529    /// Stored behind a `RefCell` because highlighting is performed during
530    /// rendering (where only `&self` is available) yet should be memoized.
531    /// Spans are reused across wrapped visual segments and across scroll-only
532    /// renders. On an edit the cache is truncated from the first changed line
533    /// (tracked via `pre_edit_line`) rather than fully cleared, so multi-line
534    /// constructs stay correct without re-parsing the whole file.
535    pub(crate) highlight_cache: RefCell<Option<HighlightCache>>,
536    /// Remaining syntax lines that may be parsed during the current content
537    /// render. `usize::MAX` keeps direct non-render uses (notably tests) uncapped.
538    pub(crate) highlight_lines_remaining: Cell<usize>,
539    /// Topmost logical line touched by the cursors/selections before the
540    /// current edit, captured at the top of `update()`.
541    ///
542    /// Used as a conservative lower bound for the first line an edit may
543    /// change, to truncate `highlight_cache` precisely.
544    pub(crate) pre_edit_line: usize,
545    /// Bottommost logical line touched before the current edit.
546    ///
547    /// Together with `pre_edit_line`, this bounds the portion of the visual-line
548    /// cache that must be rebuilt after a localized edit.
549    pub(crate) pre_edit_last_line: usize,
550}
551
552#[derive(Clone, Copy, PartialEq, Eq)]
553struct VisualLinesKey {
554    buffer_revision: u64,
555    /// `f32::to_bits()` is used so the cache key is stable and exact:
556    /// - no epsilon comparisons are required
557    /// - NaN payloads (if any) do not collapse unexpectedly
558    viewport_width_bits: u32,
559    gutter_width_bits: u32,
560    wrap_enabled: bool,
561    wrap_column: Option<usize>,
562    folding_enabled: bool,
563    fold_revision: u64,
564    full_char_width_bits: u32,
565    char_width_bits: u32,
566}
567
568struct VisualLinesCache {
569    key: VisualLinesKey,
570    visual_lines: Rc<Vec<wrapping::VisualLine>>,
571    buffer_line_count: usize,
572}
573
574/// Per-line widths plus a counted ordered index for O(log n) max updates.
575pub(crate) struct MaxContentWidthCache {
576    revision: u64,
577    line_widths: Vec<f32>,
578    width_counts: BTreeMap<u32, usize>,
579}
580
581impl MaxContentWidthCache {
582    fn add_width(&mut self, width: f32) {
583        *self.width_counts.entry(width.to_bits()).or_insert(0) += 1;
584    }
585
586    fn remove_width(&mut self, width: f32) {
587        let bits = width.to_bits();
588        let remove_entry = if let Some(count) = self.width_counts.get_mut(&bits)
589        {
590            *count = count.saturating_sub(1);
591            *count == 0
592        } else {
593            false
594        };
595        if remove_entry {
596            self.width_counts.remove(&bits);
597        }
598    }
599
600    fn max_width(&self) -> f32 {
601        self.width_counts
602            .last_key_value()
603            .map_or(0.0, |(bits, _)| f32::from_bits(*bits))
604    }
605}
606
607/// One highlighted logical line together with the syntect parser state *after*
608/// it, so highlighting can resume sequentially from any cached line.
609///
610/// Storing the post-line state is what makes multi-line constructs (block
611/// comments, multi-line strings) highlight correctly: line `N` is highlighted
612/// starting from the state left by line `N - 1`.
613struct CachedHighlightLine {
614    /// Colored token spans covering the full logical line.
615    spans: Rc<Vec<(Color, String)>>,
616    /// Syntect parse state after this line (start state for the next line).
617    parse_state: ParseState,
618    /// Syntect highlight state after this line (start state for the next line).
619    highlight_state: HighlightState,
620}
621
622/// Sequential per-line syntax-highlight cache.
623///
624/// `lines` holds a dense, valid prefix: `lines[i]` is the highlight of logical
625/// line `i`. The prefix is extended lazily as deeper lines become visible and
626/// truncated from the first edited line on each edit (see
627/// [`CodeEditor::invalidate_highlight_from`]), so an edit never forces a full
628/// re-parse from the top of the file.
629pub(crate) struct HighlightCache {
630    /// Active syntax/language identifier these lines were highlighted with.
631    syntax: String,
632    /// Dense valid prefix of highlighted lines (vector index = logical line).
633    lines: Vec<CachedHighlightLine>,
634}
635
636impl HighlightCache {
637    /// Creates an empty cache for the given syntax identifier.
638    ///
639    /// # Arguments
640    ///
641    /// * `syntax` - Active syntax/language identifier the cache is built for.
642    pub(crate) fn new(syntax: String) -> Self {
643        Self { syntax, lines: Vec::new() }
644    }
645
646    /// Returns the syntax identifier these lines were highlighted with.
647    pub(crate) fn syntax(&self) -> &str {
648        &self.syntax
649    }
650
651    /// Returns the number of highlighted logical lines (valid prefix length).
652    pub(crate) fn valid_len(&self) -> usize {
653        self.lines.len()
654    }
655
656    /// Returns the cached spans for `logical_line`, if within the valid prefix.
657    ///
658    /// # Arguments
659    ///
660    /// * `logical_line` - Index of the logical line to look up.
661    pub(crate) fn spans(
662        &self,
663        logical_line: usize,
664    ) -> Option<Rc<Vec<(Color, String)>>> {
665        self.lines.get(logical_line).map(|line| Rc::clone(&line.spans))
666    }
667
668    /// Returns the syntect state to resume highlighting the next line from.
669    ///
670    /// This is the state left after the last cached line, or `None` when the
671    /// cache is empty (highlighting then starts from the syntax's initial
672    /// state).
673    pub(crate) fn resume_state(&self) -> Option<(ParseState, HighlightState)> {
674        self.lines.last().map(|line| {
675            (line.parse_state.clone(), line.highlight_state.clone())
676        })
677    }
678
679    /// Appends one highlighted line and its post-line state to the prefix.
680    ///
681    /// # Arguments
682    ///
683    /// * `spans` - The colored token spans of the line.
684    /// * `parse_state` - Syntect parse state after the line.
685    /// * `highlight_state` - Syntect highlight state after the line.
686    pub(crate) fn push_line(
687        &mut self,
688        spans: Rc<Vec<(Color, String)>>,
689        parse_state: ParseState,
690        highlight_state: HighlightState,
691    ) {
692        self.lines.push(CachedHighlightLine {
693            spans,
694            parse_state,
695            highlight_state,
696        });
697    }
698
699    /// Truncates the valid prefix to `line`, discarding lines at index `line`
700    /// and beyond so they are re-highlighted on next access.
701    ///
702    /// # Arguments
703    ///
704    /// * `line` - First logical line to invalidate.
705    pub(crate) fn truncate(&mut self, line: usize) {
706        self.lines.truncate(line);
707    }
708}
709
710/// Messages emitted by the code editor
711#[derive(Debug, Clone)]
712pub enum Message {
713    /// Character typed
714    CharacterInput(char),
715    /// A printable key interpreted by the Vim state machine.
716    VimKey(char),
717    /// Toggle Vim behavior for this editor instance.
718    ToggleVimMode,
719    /// Requests that the host save this editor's current document.
720    WriteRequested,
721    /// Backspace pressed
722    Backspace,
723    /// Delete pressed
724    Delete,
725    /// Enter pressed
726    Enter,
727    /// Tab pressed (inserts 4 spaces)
728    Tab,
729    /// Arrow key pressed (direction, shift_pressed)
730    ArrowKey(ArrowDirection, bool),
731    /// Mouse clicked at position
732    MouseClick(iced::Point),
733    /// Mouse drag for selection
734    MouseDrag(iced::Point),
735    /// Mouse moved within the editor without dragging
736    MouseHover(iced::Point),
737    /// Mouse released
738    MouseRelease,
739    /// Double-click: select the word under the cursor
740    DoubleClick(iced::Point),
741    /// Triple-click: select the whole line under the cursor
742    TripleClick(iced::Point),
743    /// Right-clicked in the editor to position and open the context menu
744    ContextMenuRequested(iced::Point),
745    /// A configured context-menu action was selected.
746    CustomContextMenuAction(String),
747    /// Requests that the host reveal the editor's file in the system file manager.
748    RevealInFileManager,
749    /// Cut selected text
750    Cut,
751    /// Copy selected text (Ctrl+C)
752    Copy,
753    /// Paste text from clipboard (Ctrl+V)
754    Paste(String),
755    /// Delete selected text (Shift+Delete)
756    DeleteSelection,
757    /// Select the complete document
758    SelectAll,
759    /// Request redraw for cursor blink
760    Tick,
761    /// Page Up pressed
762    PageUp,
763    /// Page Down pressed
764    PageDown,
765    /// Home key pressed (move to start of line, shift_pressed)
766    Home(bool),
767    /// End key pressed (move to end of line, shift_pressed)
768    End(bool),
769    /// Ctrl+Home pressed (move to start of document)
770    CtrlHome,
771    /// Ctrl+End pressed (move to end of document)
772    CtrlEnd,
773    /// Go to an explicit logical position (line, column), both 0-based.
774    GotoPosition(usize, usize),
775    /// Open the go-to-line dialog (Cmd/Ctrl+G).
776    OpenGotoLine,
777    /// Close the go-to-line dialog.
778    CloseGotoLine,
779    /// Change the one-based line number shown in the go-to-line input.
780    GotoLineChanged(String),
781    /// Submit the current go-to-line input.
782    SubmitGotoLine,
783    /// Viewport scrolled - track scroll position
784    Scrolled(iced::widget::scrollable::Viewport),
785    /// Horizontal scrollbar scrolled (only when wrap is disabled)
786    HorizontalScrolled(iced::widget::scrollable::Viewport),
787    /// Undo last operation (Ctrl+Z)
788    Undo,
789    /// Redo last undone operation (Ctrl+Y)
790    Redo,
791    /// Open search dialog (Ctrl+F)
792    OpenSearch,
793    /// Open search and replace dialog (Ctrl+H)
794    OpenSearchReplace,
795    /// Close search dialog (Escape)
796    CloseSearch,
797    /// Search query text changed
798    SearchQueryChanged(String),
799    /// Replace text changed
800    ReplaceQueryChanged(String),
801    /// Toggle case sensitivity
802    ToggleCaseSensitive,
803    /// Find next match (F3)
804    FindNext,
805    /// Find previous match (Shift+F3)
806    FindPrevious,
807    /// Replace current match
808    ReplaceNext,
809    /// Replace all matches
810    ReplaceAll,
811    /// Tab pressed in search dialog (cycle forward)
812    SearchDialogTab,
813    /// Shift+Tab pressed in search dialog (cycle backward)
814    SearchDialogShiftTab,
815    /// Tab pressed for focus navigation (when search dialog is not open)
816    FocusNavigationTab,
817    /// Shift+Tab pressed for focus navigation (when search dialog is not open)
818    FocusNavigationShiftTab,
819    /// Canvas gained focus (mouse click)
820    CanvasFocusGained,
821    /// Canvas lost focus (external widget interaction)
822    CanvasFocusLost,
823    /// Triggered when the user performs a Ctrl+Click (or Cmd+Click on macOS)
824    /// on the editor content, intending to jump to the definition of the symbol
825    /// under the cursor.
826    JumpClick(iced::Point),
827    /// IME input method opened
828    ImeOpened,
829    /// IME pre-edit update (content, selection range)
830    ImePreedit(String, Option<Range<usize>>),
831    /// IME commit text
832    ImeCommit(String),
833    /// IME input method closed
834    ImeClosed,
835    /// Alt+Click: add a new cursor at the given canvas position
836    AltClick(iced::Point),
837    /// Ctrl+Alt+Up: add a cursor on the line above the primary cursor
838    AddCursorAbove,
839    /// Ctrl+Alt+Down: add a cursor on the line below the primary cursor
840    AddCursorBelow,
841    /// Ctrl+D: select the next occurrence of the currently selected text (or word under cursor)
842    SelectNextOccurrence,
843    /// Toggle the collapsed state of the fold whose header is the given logical line.
844    ToggleFold(usize),
845    /// Toggle the collapsed state of the innermost block containing the primary cursor.
846    ToggleFoldAtCursor,
847    /// Fold every foldable block in the buffer.
848    FoldAll,
849    /// Unfold every collapsed block in the buffer.
850    UnfoldAll,
851    /// Alt+Up: move the current line (or selected line range) up by one line.
852    MoveLineUp,
853    /// Alt+Down: move the current line (or selected line range) down by one line.
854    MoveLineDown,
855    /// Shift+Alt+Up: duplicate the current line (or selected line range) above.
856    DuplicateLineUp,
857    /// Shift+Alt+Down: duplicate the current line (or selected line range) below.
858    DuplicateLineDown,
859    /// Ctrl+/: toggle line comments on the current line or primary selection.
860    ToggleComment,
861}
862
863/// Indentation style used when pressing the Tab key.
864///
865/// Controls whether indentation inserts spaces or a tab character.
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
867pub enum IndentStyle {
868    /// Insert `n` space characters.
869    Spaces(u8),
870    /// Insert a single tab character (`\t`).
871    Tab,
872}
873
874impl IndentStyle {
875    /// All standard indentation styles available for selection.
876    pub const ALL: [IndentStyle; 4] = [
877        IndentStyle::Spaces(2),
878        IndentStyle::Spaces(4),
879        IndentStyle::Spaces(8),
880        IndentStyle::Tab,
881    ];
882}
883
884impl std::fmt::Display for IndentStyle {
885    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
886        match self {
887            IndentStyle::Spaces(1) => write!(f, "1 space"),
888            IndentStyle::Spaces(n) => write!(f, "{n} spaces"),
889            IndentStyle::Tab => write!(f, "Tab"),
890        }
891    }
892}
893
894/// Arrow key directions
895#[derive(Debug, Clone, Copy)]
896pub enum ArrowDirection {
897    Up,
898    Down,
899    Left,
900    Right,
901}
902
903impl CodeEditor {
904    /// Creates a new canvas-based text editor.
905    ///
906    /// # Arguments
907    ///
908    /// * `content` - Initial text content
909    /// * `syntax` - Syntax highlighting language (e.g., "py", "lua", "rs")
910    ///
911    /// # Returns
912    ///
913    /// A new `CodeEditor` instance
914    pub fn new(content: &str, syntax: &str) -> Self {
915        // Generate a unique ID for this editor instance
916        let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
917
918        // Give focus to the first editor created (ID == 1)
919        if editor_id == 1 {
920            FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
921        }
922
923        let mut editor = Self {
924            editor_id,
925            buffer: TextBuffer::new(content),
926            cursors: cursor_set::CursorSet::new((0, 0)),
927            horizontal_scroll_offset: 0.0,
928            style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
929            syntax: syntax.to_string(),
930            last_blink: Instant::now(),
931            cursor_visible: true,
932            is_dragging: false,
933            content_cache: canvas::Cache::default(),
934            overlay_cache: canvas::Cache::default(),
935            scrollable_id: Id::unique(),
936            horizontal_scrollable_id: Id::unique(),
937            max_content_width_cache: RefCell::new(None),
938            viewport_scroll: 0.0,
939            viewport_height: 600.0, // Default, will be updated
940            viewport_width: 800.0,  // Default, will be updated
941            history: CommandHistory::new(100),
942            is_grouping: false,
943            wrap_enabled: true,
944            auto_indent_enabled: true,
945            indent_style: IndentStyle::Spaces(4),
946            wrap_column: None,
947            folding_enabled: true,
948            collapsed_folds: HashSet::new(),
949            fold_revision: 0,
950            foldable_regions_cache: RefCell::new(None),
951            search_state: search::SearchState::new(),
952            custom_context_menu_entries: Vec::new(),
953            default_context_menu_enabled: true,
954            reveal_in_file_manager_enabled: false,
955            goto_line_state: goto_line::GotoLineState::new(),
956            vim_enabled: false,
957            vim_state: vim::VimState::default(),
958            translations: Translations::default(),
959            search_replace_enabled: true,
960            line_numbers_enabled: true,
961            show_whitespace: true,
962            lsp_enabled: true,
963            lsp_client: None,
964            lsp_document: None,
965            lsp_pending_changes: Vec::new(),
966            lsp_shadow_text: String::new(),
967            lsp_shadow_is_current: true,
968            lsp_synced_line_count: 1,
969            lsp_synced_last_line_len: 0,
970            lsp_edit_snapshot: None,
971            lsp_auto_flush: true,
972            has_canvas_focus: false,
973            focus_locked: false,
974            show_cursor: false,
975            modifiers: Cell::new(iced::keyboard::Modifiers::default()),
976            last_click: Cell::new(None),
977            font: iced::Font::MONOSPACE,
978            ime_preedit: None,
979            font_size: FONT_SIZE,
980            full_char_width: CHAR_WIDTH * 2.0,
981            line_height: LINE_HEIGHT,
982            char_width: CHAR_WIDTH,
983            // Initialize render window tracking for virtual scrolling:
984            // these indices define the cached visual line window. The window is
985            // expanded beyond the visible range to amortize redraws and keep scrolling smooth.
986            last_first_visible_line: 0,
987            cache_window_start_line: 0,
988            cache_window_end_line: 0,
989            buffer_revision: 0,
990            visual_lines_cache: RefCell::new(None),
991            highlight_cache: RefCell::new(None),
992            highlight_lines_remaining: Cell::new(usize::MAX),
993            pre_edit_line: 0,
994            pre_edit_last_line: 0,
995        };
996
997        // Perform initial character dimension calculation
998        editor.recalculate_char_dimensions(false);
999
1000        editor
1001    }
1002
1003    /// Replaces the custom context-menu entries.
1004    pub fn set_custom_context_menu_entries(
1005        &mut self,
1006        entries: Vec<ContextMenuEntry>,
1007    ) {
1008        self.custom_context_menu_entries = entries;
1009    }
1010
1011    /// Replaces the custom context-menu entries using the builder pattern.
1012    #[must_use]
1013    pub fn with_custom_context_menu_entries(
1014        mut self,
1015        entries: Vec<ContextMenuEntry>,
1016    ) -> Self {
1017        self.set_custom_context_menu_entries(entries);
1018        self
1019    }
1020
1021    /// Returns the custom context-menu entries in display order.
1022    pub fn custom_context_menu_entries(&self) -> &[ContextMenuEntry] {
1023        &self.custom_context_menu_entries
1024    }
1025
1026    /// Sets whether the built-in editing actions appear in the context menu.
1027    pub fn set_default_context_menu_enabled(&mut self, enabled: bool) {
1028        self.default_context_menu_enabled = enabled;
1029    }
1030
1031    /// Sets built-in context-menu visibility using the builder pattern.
1032    #[must_use]
1033    pub fn with_default_context_menu_enabled(mut self, enabled: bool) -> Self {
1034        self.set_default_context_menu_enabled(enabled);
1035        self
1036    }
1037
1038    /// Returns whether the built-in context-menu actions are enabled.
1039    pub fn default_context_menu_enabled(&self) -> bool {
1040        self.default_context_menu_enabled
1041    }
1042
1043    /// Sets whether the built-in reveal-in-file-manager action is shown.
1044    pub fn set_reveal_in_file_manager_enabled(&mut self, enabled: bool) {
1045        self.reveal_in_file_manager_enabled = enabled;
1046    }
1047
1048    /// Sets reveal-in-file-manager visibility using the builder pattern.
1049    #[must_use]
1050    pub fn with_reveal_in_file_manager_enabled(
1051        mut self,
1052        enabled: bool,
1053    ) -> Self {
1054        self.set_reveal_in_file_manager_enabled(enabled);
1055        self
1056    }
1057
1058    /// Returns whether the reveal-in-file-manager action is shown.
1059    pub fn reveal_in_file_manager_enabled(&self) -> bool {
1060        self.reveal_in_file_manager_enabled
1061    }
1062
1063    /// Sets the font used by the editor
1064    ///
1065    /// # Arguments
1066    ///
1067    /// * `font` - The iced font to set for the editor
1068    pub fn set_font(&mut self, font: iced::Font) {
1069        self.font = font;
1070        self.recalculate_char_dimensions(false);
1071    }
1072
1073    /// Sets the font size and recalculates character dimensions.
1074    ///
1075    /// If `auto_adjust_line_height` is true, `line_height` will also be scaled to maintain
1076    /// the default proportion (Line Height ~ 1.43x).
1077    ///
1078    /// # Arguments
1079    ///
1080    /// * `size` - The font size in pixels
1081    /// * `auto_adjust_line_height` - Whether to automatically adjust the line height
1082    pub fn set_font_size(&mut self, size: f32, auto_adjust_line_height: bool) {
1083        self.font_size = size;
1084        self.recalculate_char_dimensions(auto_adjust_line_height);
1085    }
1086
1087    /// Recalculates character dimensions based on current font and size.
1088    fn recalculate_char_dimensions(&mut self, auto_adjust_line_height: bool) {
1089        self.char_width = self.measure_single_char_width("a");
1090        // Use '汉' as a standard reference for CJK (Chinese, Japanese, Korean) wide characters
1091        self.full_char_width = self.measure_single_char_width("汉");
1092
1093        // Fallback for infinite width measurements
1094        if self.char_width.is_infinite() {
1095            self.char_width = self.font_size / 2.0; // Rough estimate for monospace
1096        }
1097
1098        if self.full_char_width.is_infinite() {
1099            self.full_char_width = self.font_size;
1100        }
1101
1102        if auto_adjust_line_height {
1103            let line_height_ratio = LINE_HEIGHT / FONT_SIZE;
1104            self.line_height = self.font_size * line_height_ratio;
1105        }
1106
1107        self.content_cache.clear();
1108        self.overlay_cache.clear();
1109        *self.max_content_width_cache.borrow_mut() = None;
1110    }
1111
1112    /// Measures the width of a single character string using the current font settings.
1113    fn measure_single_char_width(&self, content: &str) -> f32 {
1114        let text = Text {
1115            content,
1116            font: self.font,
1117            size: iced::Pixels(self.font_size),
1118            line_height: iced::advanced::text::LineHeight::default(),
1119            bounds: iced::Size::new(f32::INFINITY, f32::INFINITY),
1120            align_x: Alignment::Left,
1121            align_y: iced::alignment::Vertical::Top,
1122            shaping: iced::advanced::text::Shaping::Advanced,
1123            wrapping: iced::advanced::text::Wrapping::default(),
1124        };
1125        let p = <iced::Renderer as TextRenderer>::Paragraph::with_text(text);
1126        p.min_width()
1127    }
1128
1129    /// Returns the current font size.
1130    ///
1131    /// # Returns
1132    ///
1133    /// The font size in pixels
1134    pub fn font_size(&self) -> f32 {
1135        self.font_size
1136    }
1137
1138    /// Returns the width of a standard narrow character in pixels.
1139    ///
1140    /// # Returns
1141    ///
1142    /// The character width in pixels
1143    pub fn char_width(&self) -> f32 {
1144        self.char_width
1145    }
1146
1147    /// Returns the width of a wide character (e.g. CJK) in pixels.
1148    ///
1149    /// # Returns
1150    ///
1151    /// The full character width in pixels
1152    pub fn full_char_width(&self) -> f32 {
1153        self.full_char_width
1154    }
1155
1156    /// Measures the rendered width for a given text snippet using editor metrics.
1157    pub fn measure_text_width(&self, text: &str) -> f32 {
1158        measure_text_width(text, self.full_char_width, self.char_width)
1159    }
1160
1161    /// Sets the line height used by the editor
1162    ///
1163    /// # Arguments
1164    ///
1165    /// * `height` - The line height in pixels
1166    pub fn set_line_height(&mut self, height: f32) {
1167        self.line_height = height;
1168        self.content_cache.clear();
1169        self.overlay_cache.clear();
1170    }
1171
1172    /// Returns the current line height.
1173    ///
1174    /// # Returns
1175    ///
1176    /// The line height in pixels
1177    pub fn line_height(&self) -> f32 {
1178        self.line_height
1179    }
1180
1181    /// Returns the current viewport height in pixels.
1182    pub fn viewport_height(&self) -> f32 {
1183        self.viewport_height
1184    }
1185
1186    /// Returns the current viewport width in pixels.
1187    pub fn viewport_width(&self) -> f32 {
1188        self.viewport_width
1189    }
1190
1191    /// Returns the current vertical scroll offset in pixels.
1192    pub fn viewport_scroll(&self) -> f32 {
1193        self.viewport_scroll
1194    }
1195
1196    /// Returns the current text content as a string.
1197    ///
1198    /// # Returns
1199    ///
1200    /// The complete text content of the editor
1201    pub fn content(&self) -> String {
1202        self.buffer.to_string()
1203    }
1204
1205    /// Enables or disables Vim behavior for this editor instance.
1206    ///
1207    /// Changing this setting enters a clean Normal mode without modifying the
1208    /// buffer or command history.
1209    pub fn set_vim_enabled(&mut self, enabled: bool) {
1210        if self.is_grouping {
1211            self.history.end_group();
1212            self.is_grouping = false;
1213        }
1214        self.vim_enabled = enabled;
1215        self.vim_state.enter_clean_normal_mode();
1216        self.cursors.remove_all_but_primary();
1217        let position = if enabled {
1218            self.vim_normal_position(self.cursors.primary_position())
1219        } else {
1220            self.cursors.primary_position()
1221        };
1222        self.cursors.set_single(position);
1223        self.is_dragging = false;
1224        self.overlay_cache.clear();
1225    }
1226
1227    /// Sets whether Vim behavior is enabled using the builder pattern.
1228    #[must_use]
1229    pub fn with_vim_enabled(mut self, enabled: bool) -> Self {
1230        self.set_vim_enabled(enabled);
1231        self
1232    }
1233
1234    /// Returns whether Vim behavior is enabled for this editor instance.
1235    pub fn vim_enabled(&self) -> bool {
1236        self.vim_enabled
1237    }
1238
1239    /// Returns the active Vim mode, or `None` when Vim behavior is disabled.
1240    pub fn vim_mode(&self) -> Option<VimMode> {
1241        self.vim_enabled.then(|| self.vim_state.mode())
1242    }
1243
1244    /// Sets the viewport height for the editor.
1245    ///
1246    /// This determines the minimum height of the canvas, ensuring proper
1247    /// background rendering even when content is smaller than the viewport.
1248    ///
1249    /// # Arguments
1250    ///
1251    /// * `height` - The viewport height in pixels
1252    ///
1253    /// # Returns
1254    ///
1255    /// Self for method chaining
1256    ///
1257    /// # Example
1258    ///
1259    /// ```
1260    /// use iced_code_editor::CodeEditor;
1261    ///
1262    /// let editor = CodeEditor::new("fn main() {}", "rs")
1263    ///     .with_viewport_height(500.0);
1264    /// ```
1265    #[must_use]
1266    pub fn with_viewport_height(mut self, height: f32) -> Self {
1267        self.viewport_height = height;
1268        self
1269    }
1270
1271    /// Sets the theme style for the editor.
1272    ///
1273    /// # Arguments
1274    ///
1275    /// * `style` - The style to apply to the editor
1276    ///
1277    /// # Example
1278    ///
1279    /// ```
1280    /// use iced_code_editor::{CodeEditor, theme};
1281    ///
1282    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1283    /// editor.set_theme(theme::from_iced_theme(&iced::Theme::TokyoNightStorm));
1284    /// ```
1285    pub fn set_theme(&mut self, style: Style) {
1286        self.style = style;
1287        self.content_cache.clear();
1288        self.overlay_cache.clear();
1289    }
1290
1291    /// Sets the language for UI translations.
1292    ///
1293    /// This changes the language used for all UI text elements in the editor,
1294    /// including search dialog tooltips, placeholders, and labels.
1295    ///
1296    /// # Arguments
1297    ///
1298    /// * `language` - The language to use for UI text
1299    ///
1300    /// # Example
1301    ///
1302    /// ```
1303    /// use iced_code_editor::{CodeEditor, Language};
1304    ///
1305    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1306    /// editor.set_language(Language::French);
1307    /// ```
1308    pub fn set_language(&mut self, language: crate::i18n::Language) {
1309        self.translations.set_language(language);
1310        self.overlay_cache.clear();
1311    }
1312
1313    /// Returns the current UI language.
1314    ///
1315    /// # Returns
1316    ///
1317    /// The currently active language for UI text
1318    ///
1319    /// # Example
1320    ///
1321    /// ```
1322    /// use iced_code_editor::{CodeEditor, Language};
1323    ///
1324    /// let editor = CodeEditor::new("fn main() {}", "rs");
1325    /// let current_lang = editor.language();
1326    /// ```
1327    pub fn language(&self) -> crate::i18n::Language {
1328        self.translations.language()
1329    }
1330
1331    /// Attaches an LSP client and opens a document for the current buffer.
1332    ///
1333    /// This sends an initial `did_open` with the current buffer contents and
1334    /// resets any pending LSP change state.
1335    ///
1336    /// # Arguments
1337    ///
1338    /// * `client` - The LSP client to notify
1339    /// * `document` - Document metadata describing the buffer
1340    pub fn attach_lsp(
1341        &mut self,
1342        mut client: Box<dyn lsp::LspClient>,
1343        mut document: lsp::LspDocument,
1344    ) {
1345        if !self.lsp_enabled {
1346            return;
1347        }
1348        document.version = 1;
1349        let text = self.buffer.to_string();
1350        client.did_open(&document, &text);
1351        self.lsp_client = Some(client);
1352        self.lsp_document = Some(document);
1353        self.lsp_shadow_text = text;
1354        self.lsp_shadow_is_current = true;
1355        self.update_lsp_synced_extent();
1356        self.lsp_edit_snapshot = None;
1357        self.lsp_pending_changes.clear();
1358    }
1359
1360    /// Opens a new document on the attached LSP client.
1361    ///
1362    /// If a document is already open, this will close it before opening the new
1363    /// one and reset pending change tracking.
1364    ///
1365    /// # Arguments
1366    ///
1367    /// * `document` - Document metadata describing the buffer
1368    pub fn lsp_open_document(&mut self, mut document: lsp::LspDocument) {
1369        let Some(client) = self.lsp_client.as_mut() else { return };
1370        if let Some(current) = self.lsp_document.as_ref() {
1371            client.did_close(current);
1372        }
1373        document.version = 1;
1374        let text = self.buffer.to_string();
1375        client.did_open(&document, &text);
1376        self.lsp_document = Some(document);
1377        self.lsp_shadow_text = text;
1378        self.lsp_shadow_is_current = true;
1379        self.update_lsp_synced_extent();
1380        self.lsp_edit_snapshot = None;
1381        self.lsp_pending_changes.clear();
1382    }
1383
1384    /// Detaches the current LSP client and closes any open document.
1385    ///
1386    /// This clears all LSP-related state on the editor instance.
1387    pub fn detach_lsp(&mut self) {
1388        if let (Some(client), Some(document)) =
1389            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1390        {
1391            client.did_close(document);
1392        }
1393        self.lsp_client = None;
1394        self.lsp_document = None;
1395        self.lsp_shadow_text = String::new();
1396        self.lsp_shadow_is_current = true;
1397        self.lsp_synced_line_count = 1;
1398        self.lsp_synced_last_line_len = 0;
1399        self.lsp_edit_snapshot = None;
1400        self.lsp_pending_changes.clear();
1401    }
1402
1403    /// Sends a `did_save` notification with the current buffer contents.
1404    pub fn lsp_did_save(&mut self) {
1405        if let (Some(client), Some(document)) =
1406            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1407        {
1408            let text = self.buffer.to_string();
1409            client.did_save(document, &text);
1410        }
1411    }
1412
1413    /// Requests hover information at the current cursor position.
1414    pub fn lsp_request_hover(&mut self) {
1415        let position = self.lsp_position_from_cursor();
1416        if let (Some(client), Some(document)) =
1417            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1418        {
1419            client.request_hover(document, position);
1420        }
1421    }
1422
1423    /// Requests hover information at a canvas point.
1424    ///
1425    /// Returns `true` if the point maps to a valid buffer position and the
1426    /// request was sent.
1427    pub fn lsp_request_hover_at(&mut self, point: iced::Point) -> bool {
1428        let Some(position) = self.lsp_position_from_point(point) else {
1429            return false;
1430        };
1431        if let (Some(client), Some(document)) =
1432            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1433        {
1434            client.request_hover(document, position);
1435            return true;
1436        }
1437        false
1438    }
1439
1440    /// Requests hover information at an explicit LSP position.
1441    ///
1442    /// Returns `true` if an LSP client is attached and the request was sent.
1443    pub fn lsp_request_hover_at_position(
1444        &mut self,
1445        position: lsp::LspPosition,
1446    ) -> bool {
1447        if let (Some(client), Some(document)) =
1448            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1449        {
1450            client.request_hover(document, position);
1451            return true;
1452        }
1453        false
1454    }
1455
1456    /// Converts a canvas point to an LSP position, if possible.
1457    pub fn lsp_position_at_point(
1458        &self,
1459        point: iced::Point,
1460    ) -> Option<lsp::LspPosition> {
1461        self.lsp_position_from_point(point)
1462    }
1463
1464    /// Returns the hover anchor position and its canvas point for a given
1465    /// cursor location.
1466    ///
1467    /// The anchor is the start of the word under the cursor, which is useful
1468    /// for LSP hover and definition requests.
1469    pub fn lsp_hover_anchor_at_point(
1470        &self,
1471        point: iced::Point,
1472    ) -> Option<(lsp::LspPosition, iced::Point)> {
1473        let (line, col) = self.calculate_cursor_from_point(point)?;
1474        let line_content = self.buffer.line(line);
1475        let anchor_col = Self::word_start_in_line(line_content, col);
1476        let anchor_point =
1477            self.point_from_position(line, anchor_col).unwrap_or(point);
1478        let line = u32::try_from(line).unwrap_or(u32::MAX);
1479        let character = u32::try_from(anchor_col).unwrap_or(u32::MAX);
1480        Some((lsp::LspPosition { line, character }, anchor_point))
1481    }
1482
1483    /// Requests completion items at the current cursor position.
1484    pub fn lsp_request_completion(&mut self) {
1485        let position = self.lsp_position_from_cursor();
1486        if let (Some(client), Some(document)) =
1487            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
1488        {
1489            client.request_completion(document, position);
1490        }
1491    }
1492
1493    /// Flushes pending LSP text changes to the attached client.
1494    ///
1495    /// This increments the document version and sends `did_change` with all
1496    /// queued changes.
1497    pub fn lsp_flush_pending_changes(&mut self) {
1498        if self.lsp_pending_changes.is_empty() {
1499            return;
1500        }
1501
1502        if let (Some(client), Some(document)) =
1503            (self.lsp_client.as_mut(), self.lsp_document.as_mut())
1504        {
1505            let changes = std::mem::take(&mut self.lsp_pending_changes);
1506            document.version = document.version.saturating_add(1);
1507            client.did_change(document, &changes);
1508        }
1509    }
1510
1511    /// Sets whether LSP changes are flushed automatically after edits.
1512    pub fn set_lsp_auto_flush(&mut self, auto_flush: bool) {
1513        self.lsp_auto_flush = auto_flush;
1514    }
1515
1516    /// Requests focus for this editor.
1517    ///
1518    /// This method programmatically sets the focus to this editor instance,
1519    /// allowing it to receive keyboard events. Other editors will automatically
1520    /// lose focus.
1521    ///
1522    /// # Example
1523    ///
1524    /// ```
1525    /// use iced_code_editor::CodeEditor;
1526    ///
1527    /// let mut editor1 = CodeEditor::new("fn main() {}", "rs");
1528    /// let mut editor2 = CodeEditor::new("fn test() {}", "rs");
1529    ///
1530    /// // Give focus to editor2
1531    /// editor2.request_focus();
1532    /// ```
1533    pub fn request_focus(&self) {
1534        FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
1535    }
1536
1537    /// Checks if this editor currently has focus.
1538    ///
1539    /// Returns `true` if this editor will receive keyboard events,
1540    /// `false` otherwise.
1541    ///
1542    /// # Returns
1543    ///
1544    /// `true` if focused, `false` otherwise
1545    ///
1546    /// # Example
1547    ///
1548    /// ```
1549    /// use iced_code_editor::CodeEditor;
1550    ///
1551    /// let editor = CodeEditor::new("fn main() {}", "rs");
1552    /// if editor.is_focused() {
1553    ///     println!("Editor has focus");
1554    /// }
1555    /// ```
1556    pub fn is_focused(&self) -> bool {
1557        FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
1558    }
1559
1560    /// Resets the editor with new content.
1561    ///
1562    /// This method replaces the buffer content and resets all editor state
1563    /// (cursor position, selection, scroll, history) to initial values.
1564    /// Use this instead of creating a new `CodeEditor` instance to ensure
1565    /// proper widget tree updates in iced.
1566    ///
1567    /// Returns a `Task` that scrolls the editor to the top, which also
1568    /// forces a redraw of the canvas.
1569    ///
1570    /// # Arguments
1571    ///
1572    /// * `content` - The new text content
1573    ///
1574    /// # Returns
1575    ///
1576    /// A `Task<Message>` that should be returned from your update function
1577    ///
1578    /// # Example
1579    ///
1580    /// ```ignore
1581    /// use iced_code_editor::CodeEditor;
1582    ///
1583    /// let mut editor = CodeEditor::new("initial content", "lua");
1584    /// // Later, reset with new content and get the task
1585    /// let task = editor.reset("new content");
1586    /// // Return task.map(YourMessage::Editor) from your update function
1587    /// ```
1588    pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
1589        self.buffer = TextBuffer::new(content);
1590        self.cursors.set_single((0, 0));
1591        self.vim_state.reset();
1592        self.horizontal_scroll_offset = 0.0;
1593        self.is_dragging = false;
1594        self.viewport_scroll = 0.0;
1595        self.history = CommandHistory::new(100);
1596        self.is_grouping = false;
1597        self.last_blink = Instant::now();
1598        self.cursor_visible = true;
1599        self.content_cache = canvas::Cache::default();
1600        self.overlay_cache = canvas::Cache::default();
1601        self.buffer_revision = self.buffer_revision.wrapping_add(1);
1602        *self.visual_lines_cache.borrow_mut() = None;
1603        // The buffer is fully replaced, so discard the whole highlight prefix.
1604        self.pre_edit_line = 0;
1605        self.pre_edit_last_line = usize::MAX;
1606        self.invalidate_highlight_from(0);
1607        self.enqueue_lsp_change();
1608
1609        // Scroll to top to force a redraw
1610        snap_to(self.scrollable_id.clone(), RelativeOffset::START)
1611    }
1612
1613    /// Resets the cursor blink animation.
1614    pub(crate) fn reset_cursor_blink(&mut self) {
1615        self.last_blink = Instant::now();
1616        self.cursor_visible = true;
1617    }
1618
1619    /// Converts the current cursor position into an LSP position.
1620    fn lsp_position_from_cursor(&self) -> lsp::LspPosition {
1621        let pos = self.cursors.primary_position();
1622        let line = u32::try_from(pos.0).unwrap_or(u32::MAX);
1623        let character = u32::try_from(pos.1).unwrap_or(u32::MAX);
1624        lsp::LspPosition { line, character }
1625    }
1626
1627    /// Converts a canvas point into an LSP position, if it hits the buffer.
1628    fn lsp_position_from_point(
1629        &self,
1630        point: iced::Point,
1631    ) -> Option<lsp::LspPosition> {
1632        let (line, col) = self.calculate_cursor_from_point(point)?;
1633        let line = u32::try_from(line).unwrap_or(u32::MAX);
1634        let character = u32::try_from(col).unwrap_or(u32::MAX);
1635        Some(lsp::LspPosition { line, character })
1636    }
1637
1638    /// Converts a logical buffer position into a canvas point, if visible.
1639    fn point_from_position(
1640        &self,
1641        line: usize,
1642        col: usize,
1643    ) -> Option<iced::Point> {
1644        let visual_lines = self.visual_lines_cached(self.viewport_width);
1645        let visual_index = wrapping::WrappingCalculator::logical_to_visual(
1646            &visual_lines,
1647            line,
1648            col,
1649        )?;
1650        let visual_line = &visual_lines[visual_index];
1651        let line_content = self.buffer.line(visual_line.logical_line);
1652        let prefix_len = col.saturating_sub(visual_line.start_col);
1653        let prefix_text: String = line_content
1654            .chars()
1655            .skip(visual_line.start_col)
1656            .take(prefix_len)
1657            .collect();
1658        let x = self.gutter_width()
1659            + 5.0
1660            + measure_text_width(
1661                &prefix_text,
1662                self.full_char_width,
1663                self.char_width,
1664            );
1665        let y = visual_index as f32 * self.line_height;
1666        Some(iced::Point::new(x, y))
1667    }
1668
1669    /// Returns the word-start column in a line for a given column.
1670    ///
1671    /// Word characters include ASCII alphanumerics and underscore.
1672    pub(crate) fn word_start_in_line(line: &str, col: usize) -> usize {
1673        let chars: Vec<char> = line.chars().collect();
1674        if chars.is_empty() {
1675            return 0;
1676        }
1677        let mut idx = col.min(chars.len());
1678        if idx == chars.len() {
1679            idx = idx.saturating_sub(1);
1680        }
1681        if !Self::is_word_char(chars[idx]) {
1682            if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1683                idx -= 1;
1684            } else {
1685                return col.min(chars.len());
1686            }
1687        }
1688        while idx > 0 && Self::is_word_char(chars[idx - 1]) {
1689            idx -= 1;
1690        }
1691        idx
1692    }
1693
1694    /// Returns the word-end column in a line for a given column.
1695    pub(crate) fn word_end_in_line(line: &str, col: usize) -> usize {
1696        let chars: Vec<char> = line.chars().collect();
1697        if chars.is_empty() {
1698            return 0;
1699        }
1700        let mut idx = col.min(chars.len());
1701        if idx == chars.len() {
1702            idx = idx.saturating_sub(1);
1703        }
1704
1705        // If current char is not a word char, check if previous was (we might be just after the word)
1706        if !Self::is_word_char(chars[idx]) {
1707            if idx > 0 && Self::is_word_char(chars[idx - 1]) {
1708                // We are just after a word, so idx is the end (exclusive)
1709                // But wait, if we are at the space after "foo", idx points to space.
1710                // "foo " -> ' ' is at 3. word_end should be 3.
1711                // So if chars[idx] is not word char, and chars[idx-1] IS, then idx is the end.
1712                return idx;
1713            } else {
1714                // Not on a word
1715                return col.min(chars.len());
1716            }
1717        }
1718
1719        // If we are on a word char, scan forward
1720        while idx < chars.len() && Self::is_word_char(chars[idx]) {
1721            idx += 1;
1722        }
1723        idx
1724    }
1725
1726    /// Returns true when the character is part of an identifier-style word.
1727    pub(crate) fn is_word_char(ch: char) -> bool {
1728        ch == '_' || ch.is_alphanumeric()
1729    }
1730
1731    /// Computes and queues the latest LSP text change for the buffer.
1732    ///
1733    /// When auto-flush is enabled, this immediately sends changes.
1734    fn enqueue_lsp_change(&mut self) {
1735        if self.lsp_document.is_none() {
1736            return;
1737        }
1738
1739        let new_text = self.buffer.to_string();
1740        let change = if self.lsp_shadow_is_current {
1741            lsp::compute_text_change(&self.lsp_shadow_text, &new_text)
1742        } else {
1743            let end_line = self.lsp_synced_line_count.saturating_sub(1);
1744            Some(lsp::LspTextChange {
1745                range: lsp::LspRange {
1746                    start: lsp::LspPosition { line: 0, character: 0 },
1747                    end: lsp::LspPosition {
1748                        line: u32::try_from(end_line).unwrap_or(u32::MAX),
1749                        character: u32::try_from(self.lsp_synced_last_line_len)
1750                            .unwrap_or(u32::MAX),
1751                    },
1752                },
1753                text: new_text.clone(),
1754            })
1755        };
1756        if let Some(change) = change {
1757            self.lsp_pending_changes.push(change);
1758        }
1759        self.lsp_shadow_text = new_text;
1760        self.lsp_shadow_is_current = true;
1761        self.update_lsp_synced_extent();
1762        if self.lsp_auto_flush {
1763            self.lsp_flush_pending_changes();
1764        }
1765    }
1766
1767    /// Queues the bounded range replacement captured before a normal editor
1768    /// command. Unlike `enqueue_lsp_change`, this never serializes or diffs the
1769    /// complete document.
1770    pub(crate) fn enqueue_incremental_lsp_change(&mut self) {
1771        if self.lsp_document.is_none() {
1772            self.lsp_edit_snapshot = None;
1773            return;
1774        }
1775
1776        let Some(snapshot) = self.lsp_edit_snapshot.take() else {
1777            self.enqueue_lsp_change();
1778            return;
1779        };
1780
1781        let new_line_count = self.buffer.line_count();
1782        let start_line =
1783            snapshot.start_line.min(new_line_count.saturating_sub(1));
1784        let new_end_exclusive = if new_line_count >= snapshot.old_line_count {
1785            snapshot
1786                .old_end_exclusive
1787                .saturating_add(new_line_count - snapshot.old_line_count)
1788                .min(new_line_count)
1789        } else {
1790            snapshot
1791                .old_end_exclusive
1792                .saturating_sub(snapshot.old_line_count - new_line_count)
1793                .max(start_line.saturating_add(1))
1794                .min(new_line_count)
1795        };
1796        let text =
1797            self.buffer.line_range_to_string(start_line, new_end_exclusive);
1798        self.lsp_pending_changes.push(lsp::LspTextChange {
1799            range: lsp::LspRange {
1800                start: lsp::LspPosition {
1801                    line: u32::try_from(snapshot.start_line)
1802                        .unwrap_or(u32::MAX),
1803                    character: 0,
1804                },
1805                end: snapshot.old_end,
1806            },
1807            text,
1808        });
1809
1810        // The shadow string is intentionally not rewritten here: doing so
1811        // would reintroduce an O(document size) copy. The compact extent below
1812        // is sufficient for a rare future full-document fallback.
1813        self.lsp_shadow_text = String::new();
1814        self.lsp_shadow_is_current = false;
1815        self.update_lsp_synced_extent();
1816        if self.lsp_auto_flush {
1817            self.lsp_flush_pending_changes();
1818        }
1819    }
1820
1821    /// Updates the compact extent of the document state represented by queued
1822    /// and already-flushed LSP changes.
1823    fn update_lsp_synced_extent(&mut self) {
1824        self.lsp_synced_line_count = self.buffer.line_count();
1825        self.lsp_synced_last_line_len =
1826            self.buffer.line_len(self.lsp_synced_line_count.saturating_sub(1));
1827    }
1828
1829    /// Refreshes search matches after buffer modification.
1830    ///
1831    /// Should be called after any operation that modifies the buffer.
1832    /// If search is active, recalculates only the affected logical lines and
1833    /// selects the match closest to the current cursor position.
1834    pub(crate) fn refresh_search_matches_if_needed(&mut self) {
1835        if self.search_matches_visible() && !self.search_state.query.is_empty()
1836        {
1837            let start_line = self.pre_edit_line.saturating_sub(1);
1838            let old_end_exclusive = self.pre_edit_last_line.saturating_add(2);
1839            self.search_state.update_matches_after_edit(
1840                &self.buffer,
1841                start_line,
1842                old_end_exclusive,
1843            );
1844
1845            // Select match closest to cursor to maintain context
1846            self.search_state
1847                .select_match_near_cursor(self.cursors.primary_position());
1848        }
1849    }
1850
1851    pub(crate) fn search_matches_visible(&self) -> bool {
1852        self.search_state.is_open
1853            || (self.vim_enabled && self.vim_state.last_search().is_some())
1854    }
1855
1856    /// Returns whether the editor has unsaved changes.
1857    ///
1858    /// # Returns
1859    ///
1860    /// `true` if there are unsaved modifications, `false` otherwise
1861    pub fn is_modified(&self) -> bool {
1862        self.history.is_modified()
1863    }
1864
1865    /// Marks the current state as saved.
1866    ///
1867    /// Call this after successfully saving the file to reset the modified state.
1868    pub fn mark_saved(&mut self) {
1869        self.history.mark_saved();
1870    }
1871
1872    /// Returns whether undo is available.
1873    pub fn can_undo(&self) -> bool {
1874        self.history.can_undo()
1875    }
1876
1877    /// Returns whether redo is available.
1878    pub fn can_redo(&self) -> bool {
1879        self.history.can_redo()
1880    }
1881
1882    /// Sets whether line wrapping is enabled.
1883    ///
1884    /// When enabled, long lines will wrap at the viewport width or at a
1885    /// configured column width.
1886    ///
1887    /// # Arguments
1888    ///
1889    /// * `enabled` - Whether to enable line wrapping
1890    ///
1891    /// # Example
1892    ///
1893    /// ```
1894    /// use iced_code_editor::CodeEditor;
1895    ///
1896    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1897    /// editor.set_wrap_enabled(false); // Disable wrapping
1898    /// ```
1899    pub fn set_wrap_enabled(&mut self, enabled: bool) {
1900        if self.wrap_enabled != enabled {
1901            self.wrap_enabled = enabled;
1902            if enabled {
1903                self.horizontal_scroll_offset = 0.0;
1904            }
1905            self.content_cache.clear();
1906            self.overlay_cache.clear();
1907        }
1908    }
1909
1910    /// Returns whether line wrapping is enabled.
1911    ///
1912    /// # Returns
1913    ///
1914    /// `true` if line wrapping is enabled, `false` otherwise
1915    pub fn wrap_enabled(&self) -> bool {
1916        self.wrap_enabled
1917    }
1918
1919    /// Enables or disables visible whitespace rendering.
1920    ///
1921    /// When enabled, space characters are rendered as `·` and tab characters
1922    /// as `→`, both drawn in a dimmed color to remain non-intrusive. Toggling
1923    /// this setting clears the content cache to trigger an immediate redraw.
1924    ///
1925    /// # Arguments
1926    ///
1927    /// * `enabled` - Whether to show whitespace characters
1928    ///
1929    /// # Example
1930    ///
1931    /// ```
1932    /// use iced_code_editor::CodeEditor;
1933    ///
1934    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1935    /// editor.set_show_whitespace(true);
1936    /// ```
1937    pub fn set_show_whitespace(&mut self, enabled: bool) {
1938        if self.show_whitespace != enabled {
1939            self.show_whitespace = enabled;
1940            self.content_cache.clear();
1941        }
1942    }
1943
1944    /// Returns whether visible whitespace rendering is enabled.
1945    pub fn show_whitespace(&self) -> bool {
1946        self.show_whitespace
1947    }
1948
1949    /// Enables or disables code folding (collapse/expand blocks).
1950    ///
1951    /// When disabled, no fold chevrons are drawn and all lines are shown
1952    /// regardless of the collapsed state (which is preserved, so re-enabling
1953    /// restores the previously collapsed blocks).
1954    ///
1955    /// # Arguments
1956    ///
1957    /// * `enabled` - Whether to enable code folding
1958    ///
1959    /// # Example
1960    ///
1961    /// ```
1962    /// use iced_code_editor::CodeEditor;
1963    ///
1964    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1965    /// editor.set_folding_enabled(true);
1966    /// ```
1967    pub fn set_folding_enabled(&mut self, enabled: bool) {
1968        if self.folding_enabled != enabled {
1969            self.folding_enabled = enabled;
1970            self.bump_fold_revision();
1971        }
1972    }
1973
1974    /// Returns whether code folding is enabled.
1975    pub fn folding_enabled(&self) -> bool {
1976        self.folding_enabled
1977    }
1978
1979    /// Returns whether the region whose header is `header_line` is collapsed.
1980    pub fn is_folded(&self, header_line: usize) -> bool {
1981        self.collapsed_folds.contains(&header_line)
1982    }
1983
1984    /// Toggles the collapsed state of the foldable region whose header is
1985    /// `header_line`.
1986    ///
1987    /// The call is a no-op if `header_line` is not currently a fold header.
1988    /// When collapsing, any cursor that would land on a hidden line is moved up
1989    /// to the header line so the caret stays visible.
1990    ///
1991    /// # Arguments
1992    ///
1993    /// * `header_line` - Logical line index of the region header
1994    pub fn toggle_fold(&mut self, header_line: usize) {
1995        let regions = self.foldable_regions();
1996        if !folding::is_fold_header(&regions, header_line) {
1997            return; // Not a fold header: nothing to toggle.
1998        }
1999
2000        if self.collapsed_folds.contains(&header_line) {
2001            self.collapsed_folds.remove(&header_line);
2002        } else {
2003            self.collapsed_folds.insert(header_line);
2004        }
2005        self.after_fold_change();
2006    }
2007
2008    /// Toggles the collapsed state of the innermost foldable region containing
2009    /// `line`.
2010    ///
2011    /// Folds the region if it is expanded, unfolds it if it is collapsed. Does
2012    /// nothing if `line` is not inside any foldable region. This is the
2013    /// cursor-driven primitive used by the keyboard shortcut and mirrors a click
2014    /// on the fold chevron.
2015    ///
2016    /// # Arguments
2017    ///
2018    /// * `line` - A logical line inside (or heading) the region to toggle
2019    pub fn toggle_fold_at(&mut self, line: usize) {
2020        let regions = self.foldable_regions();
2021        let header = regions
2022            .iter()
2023            .filter(|r| r.start_line <= line && line <= r.end_line)
2024            .map(|r| r.start_line)
2025            .max();
2026        if let Some(header) = header {
2027            if self.collapsed_folds.contains(&header) {
2028                self.collapsed_folds.remove(&header);
2029            } else {
2030                self.collapsed_folds.insert(header);
2031            }
2032            self.after_fold_change();
2033        }
2034    }
2035
2036    /// Folds the innermost foldable region containing `line`.
2037    ///
2038    /// Does nothing if `line` is not inside any foldable region or the region
2039    /// is already collapsed. This is the cursor-driven counterpart to
2040    /// [`Self::toggle_fold`].
2041    ///
2042    /// # Arguments
2043    ///
2044    /// * `line` - A logical line inside (or heading) the region to fold
2045    pub fn fold_at(&mut self, line: usize) {
2046        let regions = self.foldable_regions();
2047        // Innermost containing region: the one with the greatest start line.
2048        let header = regions
2049            .iter()
2050            .filter(|r| r.start_line <= line && line <= r.end_line)
2051            .map(|r| r.start_line)
2052            .max();
2053        if let Some(header) = header
2054            && self.collapsed_folds.insert(header)
2055        {
2056            self.after_fold_change();
2057        }
2058    }
2059
2060    /// Unfolds the innermost collapsed region containing `line`.
2061    ///
2062    /// Does nothing if no collapsed region contains `line`.
2063    ///
2064    /// # Arguments
2065    ///
2066    /// * `line` - A logical line inside (or heading) the region to unfold
2067    pub fn unfold_at(&mut self, line: usize) {
2068        let regions = self.foldable_regions();
2069        let header = regions
2070            .iter()
2071            .filter(|r| {
2072                r.start_line <= line
2073                    && line <= r.end_line
2074                    && self.collapsed_folds.contains(&r.start_line)
2075            })
2076            .map(|r| r.start_line)
2077            .max();
2078        if let Some(header) = header
2079            && self.collapsed_folds.remove(&header)
2080        {
2081            self.after_fold_change();
2082        }
2083    }
2084
2085    /// Folds every foldable block in the buffer.
2086    pub fn fold_all(&mut self) {
2087        let regions = self.foldable_regions();
2088        let mut changed = false;
2089        for region in regions.iter() {
2090            changed |= self.collapsed_folds.insert(region.start_line);
2091        }
2092        if changed {
2093            self.after_fold_change();
2094        }
2095    }
2096
2097    /// Unfolds every collapsed block in the buffer.
2098    pub fn unfold_all(&mut self) {
2099        if !self.collapsed_folds.is_empty() {
2100            self.collapsed_folds.clear();
2101            self.after_fold_change();
2102        }
2103    }
2104
2105    /// Finalizes a change to the collapsed set: keeps every cursor on a visible
2106    /// line and invalidates fold-dependent caches.
2107    fn after_fold_change(&mut self) {
2108        let hidden = self.hidden_lines_set();
2109        self.move_cursors_out_of_hidden(&hidden);
2110        self.bump_fold_revision();
2111    }
2112
2113    /// Moves any cursor sitting on a hidden line up to the nearest visible line
2114    /// above it (the header of the enclosing collapsed block).
2115    fn move_cursors_out_of_hidden(&mut self, hidden: &HashSet<usize>) {
2116        if hidden.is_empty() {
2117            return;
2118        }
2119        for cursor in self.cursors.as_mut_slice() {
2120            let mut line = cursor.position.0;
2121            while line > 0 && hidden.contains(&line) {
2122                line -= 1;
2123            }
2124            if line != cursor.position.0 {
2125                cursor.position = (line, 0);
2126            }
2127        }
2128        self.cursors.sort_and_merge();
2129    }
2130
2131    /// Invalidates fold-dependent caches after a fold-state change.
2132    fn bump_fold_revision(&mut self) {
2133        self.fold_revision = self.fold_revision.wrapping_add(1);
2134        self.content_cache.clear();
2135        self.overlay_cache.clear();
2136    }
2137
2138    /// Returns the foldable regions for the current buffer, memoized by
2139    /// `buffer_revision`.
2140    ///
2141    /// Returns an empty list when folding is disabled.
2142    pub(crate) fn foldable_regions(&self) -> Rc<Vec<folding::FoldRegion>> {
2143        if !self.folding_enabled {
2144            return Rc::new(Vec::new());
2145        }
2146
2147        let mut cache = self.foldable_regions_cache.borrow_mut();
2148        if let Some((revision, regions)) = cache.as_ref()
2149            && *revision == self.buffer_revision
2150        {
2151            return regions.clone();
2152        }
2153
2154        let regions = Rc::new(folding::compute_foldable_regions(&self.buffer));
2155        *cache = Some((self.buffer_revision, regions.clone()));
2156        regions
2157    }
2158
2159    /// Returns the set of logical lines hidden by the currently collapsed folds.
2160    ///
2161    /// Empty when folding is disabled or nothing is collapsed.
2162    pub(crate) fn hidden_lines_set(&self) -> HashSet<usize> {
2163        if !self.folding_enabled || self.collapsed_folds.is_empty() {
2164            return HashSet::new();
2165        }
2166        let regions = self.foldable_regions();
2167        folding::hidden_lines(&regions, &self.collapsed_folds)
2168    }
2169
2170    /// Enables or disables automatic indentation on Enter.
2171    ///
2172    /// When enabled, pressing Enter copies the leading whitespace of the
2173    /// current line to the new line. When disabled, the cursor is placed
2174    /// at column 0 on the new line.
2175    ///
2176    /// # Arguments
2177    ///
2178    /// * `enabled` - `true` to enable auto-indentation, `false` to disable
2179    pub fn set_auto_indent_enabled(&mut self, enabled: bool) {
2180        self.auto_indent_enabled = enabled;
2181    }
2182
2183    /// Returns whether auto-indentation is enabled.
2184    ///
2185    /// # Returns
2186    ///
2187    /// `true` if auto-indentation is enabled, `false` otherwise
2188    pub fn auto_indent_enabled(&self) -> bool {
2189        self.auto_indent_enabled
2190    }
2191
2192    /// Sets the indentation style used when pressing the Tab key.
2193    ///
2194    /// # Arguments
2195    ///
2196    /// * `style` - The indentation style (`IndentStyle::Spaces(n)` or `IndentStyle::Tab`)
2197    pub fn set_indent_style(&mut self, style: IndentStyle) {
2198        self.indent_style = style;
2199    }
2200
2201    /// Returns the current indentation style.
2202    ///
2203    /// # Returns
2204    ///
2205    /// The current [`IndentStyle`] configured for this editor
2206    pub fn indent_style(&self) -> IndentStyle {
2207        self.indent_style
2208    }
2209
2210    /// Enables or disables the search/replace functionality.
2211    ///
2212    /// When disabled, search/replace keyboard shortcuts (Ctrl+F, Ctrl+H, F3)
2213    /// will be ignored. If the search dialog is currently open, it will be closed.
2214    ///
2215    /// # Arguments
2216    ///
2217    /// * `enabled` - Whether to enable search/replace functionality
2218    ///
2219    /// # Example
2220    ///
2221    /// ```
2222    /// use iced_code_editor::CodeEditor;
2223    ///
2224    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
2225    /// editor.set_search_replace_enabled(false); // Disable search/replace
2226    /// ```
2227    pub fn set_search_replace_enabled(&mut self, enabled: bool) {
2228        self.search_replace_enabled = enabled;
2229        if !enabled && self.search_state.is_open {
2230            self.search_state.close();
2231        }
2232    }
2233
2234    /// Returns whether search/replace functionality is enabled.
2235    ///
2236    /// # Returns
2237    ///
2238    /// `true` if search/replace is enabled, `false` otherwise
2239    pub fn search_replace_enabled(&self) -> bool {
2240        self.search_replace_enabled
2241    }
2242
2243    /// Sets whether LSP support is enabled.
2244    ///
2245    /// When set to `false`, any attached LSP client is detached automatically.
2246    /// Calling [`attach_lsp`] while disabled is a no-op.
2247    ///
2248    /// # Example
2249    ///
2250    /// ```
2251    /// use iced_code_editor::CodeEditor;
2252    ///
2253    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
2254    /// editor.set_lsp_enabled(false);
2255    /// ```
2256    ///
2257    /// [`attach_lsp`]: CodeEditor::attach_lsp
2258    pub fn set_lsp_enabled(&mut self, enabled: bool) {
2259        self.lsp_enabled = enabled;
2260        if !enabled {
2261            self.detach_lsp();
2262        }
2263    }
2264
2265    /// Returns whether LSP support is enabled.
2266    ///
2267    /// `true` if LSP is enabled, `false` otherwise
2268    pub fn lsp_enabled(&self) -> bool {
2269        self.lsp_enabled
2270    }
2271
2272    /// Returns the syntax highlighting language identifier for this editor.
2273    ///
2274    /// This is the language key passed at construction (e.g., `"lua"`, `"rs"`, `"py"`).
2275    ///
2276    /// # Examples
2277    ///
2278    /// ```
2279    /// use iced_code_editor::CodeEditor;
2280    /// let editor = CodeEditor::new("fn main() {}", "rs");
2281    /// assert_eq!(editor.syntax(), "rs");
2282    /// ```
2283    pub fn syntax(&self) -> &str {
2284        &self.syntax
2285    }
2286
2287    /// Opens the search dialog programmatically.
2288    ///
2289    /// This is useful when wiring your own UI button instead of relying on
2290    /// keyboard shortcuts.
2291    ///
2292    /// # Returns
2293    ///
2294    /// A `Task<Message>` that focuses the search input.
2295    pub fn open_search_dialog(&mut self) -> iced::Task<Message> {
2296        self.update(&Message::OpenSearch)
2297    }
2298
2299    /// Opens the search-and-replace dialog programmatically.
2300    ///
2301    /// This is useful when wiring your own UI button instead of relying on
2302    /// keyboard shortcuts.
2303    ///
2304    /// # Returns
2305    ///
2306    /// A `Task<Message>` that focuses the search input.
2307    pub fn open_search_replace_dialog(&mut self) -> iced::Task<Message> {
2308        self.update(&Message::OpenSearchReplace)
2309    }
2310
2311    /// Closes the search dialog programmatically.
2312    ///
2313    /// # Returns
2314    ///
2315    /// A `Task<Message>` for any follow-up UI work.
2316    pub fn close_search_dialog(&mut self) -> iced::Task<Message> {
2317        self.update(&Message::CloseSearch)
2318    }
2319
2320    /// Opens the go-to-line dialog programmatically.
2321    ///
2322    /// The input is pre-filled with the current one-based line number.
2323    pub fn open_goto_line_dialog(&mut self) -> iced::Task<Message> {
2324        self.update(&Message::OpenGotoLine)
2325    }
2326
2327    /// Closes the go-to-line dialog programmatically.
2328    pub fn close_goto_line_dialog(&mut self) -> iced::Task<Message> {
2329        self.update(&Message::CloseGotoLine)
2330    }
2331
2332    /// Sets the line wrapping with builder pattern.
2333    ///
2334    /// # Arguments
2335    ///
2336    /// * `enabled` - Whether to enable line wrapping
2337    ///
2338    /// # Returns
2339    ///
2340    /// Self for method chaining
2341    ///
2342    /// # Example
2343    ///
2344    /// ```
2345    /// use iced_code_editor::CodeEditor;
2346    ///
2347    /// let editor = CodeEditor::new("fn main() {}", "rs")
2348    ///     .with_wrap_enabled(false);
2349    /// ```
2350    #[must_use]
2351    pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
2352        self.wrap_enabled = enabled;
2353        self
2354    }
2355
2356    /// Enables or disables code folding using the builder pattern.
2357    ///
2358    /// # Arguments
2359    ///
2360    /// * `enabled` - Whether to enable code folding
2361    ///
2362    /// # Example
2363    ///
2364    /// ```
2365    /// use iced_code_editor::CodeEditor;
2366    ///
2367    /// let editor = CodeEditor::new("fn main() {}", "rs")
2368    ///     .with_folding_enabled(true);
2369    /// ```
2370    #[must_use]
2371    pub fn with_folding_enabled(mut self, enabled: bool) -> Self {
2372        self.folding_enabled = enabled;
2373        self
2374    }
2375
2376    /// Sets the wrap column (fixed width wrapping).
2377    ///
2378    /// When set to `Some(n)`, lines will wrap at column `n`.
2379    /// When set to `None`, lines will wrap at the viewport width.
2380    ///
2381    /// # Arguments
2382    ///
2383    /// * `column` - The column to wrap at, or None for viewport-based wrapping
2384    ///
2385    /// # Example
2386    ///
2387    /// ```
2388    /// use iced_code_editor::CodeEditor;
2389    ///
2390    /// let editor = CodeEditor::new("fn main() {}", "rs")
2391    ///     .with_wrap_column(Some(80)); // Wrap at 80 characters
2392    /// ```
2393    #[must_use]
2394    pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
2395        self.wrap_column = column;
2396        self
2397    }
2398
2399    /// Sets whether line numbers are displayed.
2400    ///
2401    /// When disabled, the gutter is completely removed (0px width),
2402    /// providing more space for code display.
2403    ///
2404    /// # Arguments
2405    ///
2406    /// * `enabled` - Whether to display line numbers
2407    ///
2408    /// # Example
2409    ///
2410    /// ```
2411    /// use iced_code_editor::CodeEditor;
2412    ///
2413    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
2414    /// editor.set_line_numbers_enabled(false); // Hide line numbers
2415    /// ```
2416    pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
2417        if self.line_numbers_enabled != enabled {
2418            self.line_numbers_enabled = enabled;
2419            self.content_cache.clear();
2420            self.overlay_cache.clear();
2421        }
2422    }
2423
2424    /// Returns whether line numbers are displayed.
2425    ///
2426    /// # Returns
2427    ///
2428    /// `true` if line numbers are displayed, `false` otherwise
2429    pub fn line_numbers_enabled(&self) -> bool {
2430        self.line_numbers_enabled
2431    }
2432
2433    /// Sets the line numbers display with builder pattern.
2434    ///
2435    /// # Arguments
2436    ///
2437    /// * `enabled` - Whether to display line numbers
2438    ///
2439    /// # Returns
2440    ///
2441    /// Self for method chaining
2442    ///
2443    /// # Example
2444    ///
2445    /// ```
2446    /// use iced_code_editor::CodeEditor;
2447    ///
2448    /// let editor = CodeEditor::new("fn main() {}", "rs")
2449    ///     .with_line_numbers_enabled(false);
2450    /// ```
2451    #[must_use]
2452    pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
2453        self.line_numbers_enabled = enabled;
2454        self
2455    }
2456
2457    /// Returns the total gutter width, including the line-number area and the
2458    /// fold margin.
2459    ///
2460    /// The fold margin is added when folding is enabled, independently of line
2461    /// numbers, so fold chevrons remain clickable even without line numbers.
2462    pub(crate) fn gutter_width(&self) -> f32 {
2463        self.line_number_gutter_width() + self.fold_margin_width()
2464    }
2465
2466    /// Returns the width of the line-number area (excluding the fold margin).
2467    pub(crate) fn line_number_gutter_width(&self) -> f32 {
2468        if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
2469    }
2470
2471    /// Returns the width of the fold margin (the chevron column), or `0.0` when
2472    /// folding is disabled.
2473    pub(crate) fn fold_margin_width(&self) -> f32 {
2474        if self.folding_enabled { FOLD_MARGIN_WIDTH } else { 0.0 }
2475    }
2476
2477    /// Removes canvas focus from this editor.
2478    ///
2479    /// This method programmatically removes focus from the canvas, preventing
2480    /// it from receiving keyboard events. The cursor will be hidden, but the
2481    /// selection will remain visible.
2482    ///
2483    /// Call this when focus should move to another widget (e.g., text input).
2484    ///
2485    /// # Example
2486    ///
2487    /// ```
2488    /// use iced_code_editor::CodeEditor;
2489    ///
2490    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
2491    /// editor.lose_focus();
2492    /// ```
2493    pub fn lose_focus(&mut self) {
2494        self.has_canvas_focus = false;
2495        self.show_cursor = false;
2496        self.ime_preedit = None;
2497    }
2498
2499    /// Resets the focus lock state.
2500    ///
2501    /// This method can be called to manually unlock focus processing
2502    /// after a focus transition has completed. This is useful when
2503    /// you want to allow the editor to process input again after
2504    /// programmatic focus changes.
2505    ///
2506    /// # Example
2507    ///
2508    /// ```
2509    /// use iced_code_editor::CodeEditor;
2510    ///
2511    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
2512    /// editor.reset_focus_lock();
2513    /// ```
2514    pub fn reset_focus_lock(&mut self) {
2515        self.focus_locked = false;
2516    }
2517
2518    /// Returns the screen position of the cursor.
2519    ///
2520    /// This method returns the (x, y) coordinates of the current cursor position
2521    /// relative to the editor canvas, accounting for gutter width and line height.
2522    ///
2523    /// # Returns
2524    ///
2525    /// An `Option<iced::Point>` containing the cursor position, or `None` if
2526    /// the cursor position cannot be determined.
2527    ///
2528    /// # Example
2529    ///
2530    /// ```
2531    /// use iced_code_editor::CodeEditor;
2532    ///
2533    /// let editor = CodeEditor::new("fn main() {}", "rs");
2534    /// if let Some(point) = editor.cursor_screen_position() {
2535    ///     println!("Cursor at: ({}, {})", point.x, point.y);
2536    /// }
2537    /// ```
2538    pub fn cursor_screen_position(&self) -> Option<iced::Point> {
2539        let pos = self.cursors.primary_position();
2540        self.point_from_position(pos.0, pos.1)
2541    }
2542
2543    /// Returns the current cursor position as (line, column).
2544    ///
2545    /// This method returns the logical cursor position in the buffer,
2546    /// where line and column are both 0-indexed.
2547    ///
2548    /// # Returns
2549    ///
2550    /// A tuple `(line, column)` representing the cursor position.
2551    ///
2552    /// # Example
2553    ///
2554    /// ```
2555    /// use iced_code_editor::CodeEditor;
2556    ///
2557    /// let editor = CodeEditor::new("fn main() {}", "rs");
2558    /// let (line, col) = editor.cursor_position();
2559    /// println!("Cursor at line {}, column {}", line, col);
2560    /// ```
2561    pub fn cursor_position(&self) -> (usize, usize) {
2562        self.cursors.primary_position()
2563    }
2564
2565    /// Returns the maximum content width across all lines, in pixels.
2566    ///
2567    /// Used to size the horizontal scrollbar when `wrap_enabled = false`.
2568    /// The result is cached keyed by `buffer_revision` so repeated calls are cheap.
2569    ///
2570    /// # Returns
2571    ///
2572    /// Total width in pixels including gutter, padding and a right margin.
2573    pub(crate) fn max_content_width(&self) -> f32 {
2574        let mut cache = self.max_content_width_cache.borrow_mut();
2575        if cache
2576            .as_ref()
2577            .is_none_or(|existing| existing.revision != self.buffer_revision)
2578        {
2579            let line_widths: Vec<f32> = (0..self.buffer.line_count())
2580                .map(|line| {
2581                    measure_text_width(
2582                        self.buffer.line(line),
2583                        self.full_char_width,
2584                        self.char_width,
2585                    )
2586                })
2587                .collect();
2588            let mut width_counts = BTreeMap::new();
2589            for width in &line_widths {
2590                *width_counts.entry(width.to_bits()).or_insert(0) += 1;
2591            }
2592            *cache = Some(MaxContentWidthCache {
2593                revision: self.buffer_revision,
2594                line_widths,
2595                width_counts,
2596            });
2597        }
2598
2599        let gutter = self.gutter_width();
2600        let max_line_width =
2601            cache.as_ref().map_or(0.0, MaxContentWidthCache::max_width);
2602
2603        // gutter + left padding + text + right margin
2604        gutter + 5.0 + max_line_width + 20.0
2605    }
2606
2607    /// Returns wrapped "visual lines" for the current buffer and layout, with memoization.
2608    ///
2609    /// The editor frequently needs the wrapped view of the buffer:
2610    /// - hit-testing (mouse selection, cursor placement)
2611    /// - mapping logical ↔ visual positions
2612    /// - rendering (text, line numbers, highlights)
2613    ///
2614    /// Computing visual lines is relatively expensive for large files, so we
2615    /// cache the result keyed by:
2616    /// - `buffer_revision` (buffer content changes)
2617    /// - viewport width / gutter width (layout changes)
2618    /// - wrapping settings (wrap enabled / wrap column)
2619    /// - measured character widths (font / size changes)
2620    ///
2621    /// The returned `Rc<Vec<VisualLine>>` is cheap to clone and allows multiple
2622    /// rendering passes (content + overlay layers) to share the same computed
2623    /// layout without extra allocation.
2624    pub(crate) fn visual_lines_cached(
2625        &self,
2626        viewport_width: f32,
2627    ) -> Rc<Vec<wrapping::VisualLine>> {
2628        let key = VisualLinesKey {
2629            buffer_revision: self.buffer_revision,
2630            viewport_width_bits: viewport_width.to_bits(),
2631            gutter_width_bits: self.gutter_width().to_bits(),
2632            wrap_enabled: self.wrap_enabled,
2633            wrap_column: self.wrap_column,
2634            folding_enabled: self.folding_enabled,
2635            fold_revision: self.fold_revision,
2636            full_char_width_bits: self.full_char_width.to_bits(),
2637            char_width_bits: self.char_width.to_bits(),
2638        };
2639
2640        let mut cache = self.visual_lines_cache.borrow_mut();
2641        if let Some(existing) = cache.as_ref()
2642            && existing.key == key
2643        {
2644            return existing.visual_lines.clone();
2645        }
2646
2647        let hidden = self.hidden_lines_set();
2648        let wrapping_calc = wrapping::WrappingCalculator::new(
2649            self.wrap_enabled,
2650            self.wrap_column,
2651            self.full_char_width,
2652            self.char_width,
2653        );
2654        let visual_lines = wrapping_calc.calculate_visual_lines(
2655            &self.buffer,
2656            viewport_width,
2657            self.gutter_width(),
2658            &hidden,
2659        );
2660        let visual_lines = Rc::new(visual_lines);
2661
2662        *cache = Some(VisualLinesCache {
2663            key,
2664            visual_lines: visual_lines.clone(),
2665            buffer_line_count: self.buffer.line_count(),
2666        });
2667        visual_lines
2668    }
2669
2670    /// Rebuilds only the logical-line slice affected by the latest edit.
2671    ///
2672    /// The common typing path changes one line. Reusing the unchanged prefix
2673    /// and suffix prevents wrapping work from scaling with total file size.
2674    /// Collapsed folds intentionally fall back to a full rebuild because an
2675    /// indentation edit can change which distant lines are hidden.
2676    pub(crate) fn refresh_visual_lines_after_edit(
2677        &self,
2678        previous_revision: u64,
2679    ) {
2680        if !self.collapsed_folds.is_empty() {
2681            *self.visual_lines_cache.borrow_mut() = None;
2682            return;
2683        }
2684
2685        let mut cache_guard = self.visual_lines_cache.borrow_mut();
2686        let Some(cache) = cache_guard.as_mut() else { return };
2687        if cache.key.buffer_revision != previous_revision {
2688            *cache_guard = None;
2689            return;
2690        }
2691
2692        let same_layout = cache.key.gutter_width_bits
2693            == self.gutter_width().to_bits()
2694            && cache.key.wrap_enabled == self.wrap_enabled
2695            && cache.key.wrap_column == self.wrap_column
2696            && cache.key.folding_enabled == self.folding_enabled
2697            && cache.key.fold_revision == self.fold_revision
2698            && cache.key.full_char_width_bits == self.full_char_width.to_bits()
2699            && cache.key.char_width_bits == self.char_width.to_bits();
2700        if !same_layout {
2701            *cache_guard = None;
2702            return;
2703        }
2704
2705        let old_line_count = cache.buffer_line_count;
2706        let new_line_count = self.buffer.line_count();
2707        let start_line =
2708            self.pre_edit_line.saturating_sub(1).min(old_line_count);
2709        let old_end_line =
2710            self.pre_edit_last_line.saturating_add(2).min(old_line_count);
2711        let new_end_line = if new_line_count >= old_line_count {
2712            old_end_line
2713                .saturating_add(new_line_count - old_line_count)
2714                .min(new_line_count)
2715        } else {
2716            old_end_line
2717                .saturating_sub(old_line_count - new_line_count)
2718                .max(start_line)
2719                .min(new_line_count)
2720        };
2721
2722        let prefix_end = cache
2723            .visual_lines
2724            .partition_point(|visual| visual.logical_line < start_line);
2725        let suffix_start = cache
2726            .visual_lines
2727            .partition_point(|visual| visual.logical_line < old_end_line);
2728
2729        let wrapping_calc = wrapping::WrappingCalculator::new(
2730            self.wrap_enabled,
2731            self.wrap_column,
2732            self.full_char_width,
2733            self.char_width,
2734        );
2735        let changed_visual_lines = wrapping_calc.calculate_visual_lines_range(
2736            &self.buffer,
2737            f32::from_bits(cache.key.viewport_width_bits),
2738            f32::from_bits(cache.key.gutter_width_bits),
2739            &HashSet::new(),
2740            start_line..new_end_line,
2741        );
2742
2743        let old_segment_count = suffix_start.saturating_sub(prefix_end);
2744        let new_segment_count = changed_visual_lines.len();
2745        let visual_lines = Rc::make_mut(&mut cache.visual_lines);
2746
2747        // The overwhelmingly common typing case keeps both the logical-line
2748        // count and the number of wrapped segments stable. Update that tiny
2749        // slice in place, without allocating or moving the rest of the file.
2750        if new_line_count == old_line_count
2751            && old_segment_count == new_segment_count
2752        {
2753            visual_lines[prefix_end..suffix_start]
2754                .clone_from_slice(&changed_visual_lines);
2755        } else {
2756            visual_lines.splice(prefix_end..suffix_start, changed_visual_lines);
2757
2758            let shifted_suffix_start = prefix_end + new_segment_count;
2759            for visual in &mut visual_lines[shifted_suffix_start..] {
2760                visual.logical_line = if new_line_count >= old_line_count {
2761                    visual
2762                        .logical_line
2763                        .saturating_add(new_line_count - old_line_count)
2764                } else {
2765                    visual
2766                        .logical_line
2767                        .saturating_sub(old_line_count - new_line_count)
2768                };
2769            }
2770        }
2771
2772        cache.key.buffer_revision = self.buffer_revision;
2773        cache.buffer_line_count = new_line_count;
2774    }
2775
2776    /// Updates the horizontal-width index for the lines affected by an edit.
2777    ///
2778    /// This removes the final whole-file pass that used to happen after every
2779    /// keystroke when wrapping was disabled.
2780    pub(crate) fn refresh_max_content_width_after_edit(
2781        &self,
2782        previous_revision: u64,
2783    ) {
2784        let mut cache_guard = self.max_content_width_cache.borrow_mut();
2785        let Some(cache) = cache_guard.as_mut() else { return };
2786        if cache.revision != previous_revision {
2787            *cache_guard = None;
2788            return;
2789        }
2790
2791        let old_line_count = cache.line_widths.len();
2792        let new_line_count = self.buffer.line_count();
2793        let start_line =
2794            self.pre_edit_line.saturating_sub(1).min(old_line_count);
2795        let old_end_line =
2796            self.pre_edit_last_line.saturating_add(2).min(old_line_count);
2797        if start_line == 0 && old_end_line == old_line_count {
2798            *cache_guard = None;
2799            return;
2800        }
2801
2802        let new_end_line = if new_line_count >= old_line_count {
2803            old_end_line
2804                .saturating_add(new_line_count - old_line_count)
2805                .min(new_line_count)
2806        } else {
2807            old_end_line
2808                .saturating_sub(old_line_count - new_line_count)
2809                .max(start_line)
2810                .min(new_line_count)
2811        };
2812        let old_widths = cache.line_widths[start_line..old_end_line].to_vec();
2813        let new_widths: Vec<f32> = (start_line..new_end_line)
2814            .map(|line| {
2815                measure_text_width(
2816                    self.buffer.line(line),
2817                    self.full_char_width,
2818                    self.char_width,
2819                )
2820            })
2821            .collect();
2822
2823        for width in old_widths {
2824            cache.remove_width(width);
2825        }
2826        for width in &new_widths {
2827            cache.add_width(*width);
2828        }
2829        cache.line_widths.splice(start_line..old_end_line, new_widths);
2830        cache.revision = self.buffer_revision;
2831    }
2832
2833    /// Initiates a "Go to Definition" request for the symbol at the current cursor position.
2834    ///
2835    /// This method converts the current cursor coordinates into an LSP-compatible position
2836    /// and delegates the request to the active `LspClient`, if one is attached.
2837    pub fn lsp_request_definition(&mut self) {
2838        let position = self.lsp_position_from_cursor();
2839        if let (Some(client), Some(document)) =
2840            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
2841        {
2842            client.request_definition(document, position);
2843        }
2844    }
2845
2846    /// Initiates a "Go to Definition" request for the symbol at the specified screen coordinates.
2847    ///
2848    /// This is typically used for mouse interactions (e.g., Ctrl+Click). It first resolves
2849    /// the screen coordinates to a text position and then sends the request.
2850    ///
2851    /// # Returns
2852    ///
2853    /// `true` if the request was successfully sent (i.e., a valid position was found and an LSP client is active),
2854    /// `false` otherwise.
2855    pub fn lsp_request_definition_at(&mut self, point: iced::Point) -> bool {
2856        let Some(position) = self.lsp_position_from_point(point) else {
2857            return false;
2858        };
2859        if let (Some(client), Some(document)) =
2860            (self.lsp_client.as_mut(), self.lsp_document.as_ref())
2861        {
2862            client.request_definition(document, position);
2863            return true;
2864        }
2865        false
2866    }
2867}
2868
2869#[cfg(test)]
2870mod tests {
2871    use super::*;
2872    use std::cell::RefCell;
2873    use std::rc::Rc;
2874
2875    #[test]
2876    fn test_custom_context_menu_configuration() {
2877        let custom_entries = vec![
2878            ContextMenuEntry::item("format", "Format document")
2879                .with_shortcut("Shift+Alt+F"),
2880            ContextMenuEntry::separator(),
2881            ContextMenuEntry::Item(
2882                ContextMenuItem::new("rename", "Rename symbol")
2883                    .with_enabled(false),
2884            ),
2885        ];
2886
2887        let editor = CodeEditor::new("", "rs")
2888            .with_custom_context_menu_entries(custom_entries.clone())
2889            .with_default_context_menu_enabled(false);
2890
2891        assert_eq!(editor.custom_context_menu_entries(), custom_entries);
2892        assert!(!editor.default_context_menu_enabled());
2893
2894        let default_editor = CodeEditor::new("", "rs");
2895        assert!(default_editor.custom_context_menu_entries().is_empty());
2896        assert!(default_editor.default_context_menu_enabled());
2897    }
2898
2899    #[test]
2900    fn test_reveal_in_file_manager_configuration() {
2901        let mut editor = CodeEditor::new("", "rs");
2902        assert!(!editor.reveal_in_file_manager_enabled());
2903
2904        editor.set_reveal_in_file_manager_enabled(true);
2905        assert!(editor.reveal_in_file_manager_enabled());
2906
2907        let editor =
2908            CodeEditor::new("", "rs").with_reveal_in_file_manager_enabled(true);
2909        assert!(editor.reveal_in_file_manager_enabled());
2910    }
2911
2912    #[test]
2913    fn vim_disabled_by_default() {
2914        let editor = CodeEditor::new("unchanged", "rs");
2915
2916        assert!(!editor.vim_enabled());
2917        assert_eq!(editor.vim_mode(), None);
2918        assert_eq!(editor.content(), "unchanged");
2919        assert!(!editor.can_undo());
2920        assert!(!editor.can_redo());
2921    }
2922
2923    #[test]
2924    fn vim_enable_enters_clean_normal_mode() {
2925        let mut editor = CodeEditor::new("unchanged", "rs");
2926        assert_eq!(editor.vim_state.parse_key('9'), None);
2927        assert_eq!(editor.vim_state.parse_key('d'), None);
2928
2929        editor.set_vim_enabled(true);
2930
2931        assert!(editor.vim_enabled());
2932        assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
2933        assert_eq!(
2934            editor.vim_state.parse_key('l'),
2935            Some(vim::VimAction::Motion {
2936                motion: vim::VimMotion::Right,
2937                count: 1,
2938                explicit_count: false,
2939            })
2940        );
2941        assert_eq!(editor.content(), "unchanged");
2942        assert!(!editor.can_undo());
2943        assert!(!editor.can_redo());
2944    }
2945
2946    #[test]
2947    fn vim_disable_clears_pending_state() {
2948        let mut editor =
2949            CodeEditor::new("unchanged", "rs").with_vim_enabled(true);
2950        assert_eq!(editor.vim_state.parse_key('4'), None);
2951        assert_eq!(editor.vim_state.parse_key('d'), None);
2952
2953        editor.set_vim_enabled(false);
2954        assert!(!editor.vim_enabled());
2955        assert_eq!(editor.vim_mode(), None);
2956
2957        editor.set_vim_enabled(true);
2958        assert_eq!(
2959            editor.vim_state.parse_key('w'),
2960            Some(vim::VimAction::Motion {
2961                motion: vim::VimMotion::WordForward,
2962                count: 1,
2963                explicit_count: false,
2964            })
2965        );
2966        assert_eq!(editor.content(), "unchanged");
2967        assert!(!editor.can_undo());
2968        assert!(!editor.can_redo());
2969    }
2970
2971    #[test]
2972    fn vim_reset_clears_pending_state() {
2973        let mut editor = CodeEditor::new("before", "rs").with_vim_enabled(true);
2974        assert_eq!(editor.vim_state.parse_key('3'), None);
2975        assert_eq!(editor.vim_state.parse_key('g'), None);
2976
2977        let _ = editor.reset("after");
2978
2979        assert_eq!(editor.vim_mode(), Some(VimMode::Normal));
2980        assert_eq!(editor.vim_state.parse_key('g'), None);
2981        assert_eq!(
2982            editor.vim_state.parse_key('g'),
2983            Some(vim::VimAction::Motion {
2984                motion: vim::VimMotion::DocumentStart,
2985                count: 1,
2986                explicit_count: false,
2987            })
2988        );
2989        assert_eq!(editor.content(), "after");
2990        assert!(!editor.can_undo());
2991        assert!(!editor.can_redo());
2992    }
2993
2994    #[test]
2995    fn test_compare_floats() {
2996        // Equal cases
2997        assert_eq!(
2998            compare_floats(1.0, 1.0),
2999            CmpOrdering::Equal,
3000            "Exact equality"
3001        );
3002        assert_eq!(
3003            compare_floats(1.0, 1.0 + 0.0001),
3004            CmpOrdering::Equal,
3005            "Within epsilon (positive)"
3006        );
3007        assert_eq!(
3008            compare_floats(1.0, 1.0 - 0.0001),
3009            CmpOrdering::Equal,
3010            "Within epsilon (negative)"
3011        );
3012
3013        // Greater cases
3014        assert_eq!(
3015            compare_floats(1.0 + 0.002, 1.0),
3016            CmpOrdering::Greater,
3017            "Definitely greater"
3018        );
3019        assert_eq!(
3020            compare_floats(1.0011, 1.0),
3021            CmpOrdering::Greater,
3022            "Just above epsilon"
3023        );
3024
3025        // Less cases
3026        assert_eq!(
3027            compare_floats(1.0, 1.0 + 0.002),
3028            CmpOrdering::Less,
3029            "Definitely less"
3030        );
3031        assert_eq!(
3032            compare_floats(1.0, 1.0011),
3033            CmpOrdering::Less,
3034            "Just below negative epsilon"
3035        );
3036    }
3037
3038    #[test]
3039    fn test_measure_text_width_ascii() {
3040        // "abc" (3 chars) -> 3 * CHAR_WIDTH
3041        let text = "abc";
3042        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3043        let expected = CHAR_WIDTH * 3.0;
3044        assert_eq!(
3045            compare_floats(width, expected),
3046            CmpOrdering::Equal,
3047            "Width mismatch for ASCII"
3048        );
3049    }
3050
3051    #[test]
3052    fn test_measure_text_width_cjk() {
3053        // "你好" (2 chars) -> 2 * FONT_SIZE
3054        // Chinese characters are typically full-width.
3055        // width = 2 * FONT_SIZE
3056        let text = "你好";
3057        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3058        let expected = FONT_SIZE * 2.0;
3059        assert_eq!(
3060            compare_floats(width, expected),
3061            CmpOrdering::Equal,
3062            "Width mismatch for CJK"
3063        );
3064    }
3065
3066    #[test]
3067    fn test_measure_text_width_mixed() {
3068        // "Hi" (2 chars) -> 2 * CHAR_WIDTH
3069        // "你好" (2 chars) -> 2 * FONT_SIZE
3070        let text = "Hi你好";
3071        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3072        let expected = CHAR_WIDTH * 2.0 + FONT_SIZE * 2.0;
3073        assert_eq!(
3074            compare_floats(width, expected),
3075            CmpOrdering::Equal,
3076            "Width mismatch for mixed content"
3077        );
3078    }
3079
3080    #[test]
3081    fn test_measure_text_width_control_chars() {
3082        // "\t\n" (2 chars)
3083        // width = 4 * CHAR_WIDTH (tab) + 0 (newline)
3084        let text = "\t\n";
3085        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3086        let expected = CHAR_WIDTH * TAB_WIDTH as f32;
3087        assert_eq!(
3088            compare_floats(width, expected),
3089            CmpOrdering::Equal,
3090            "Width mismatch for control chars"
3091        );
3092    }
3093
3094    #[test]
3095    fn test_measure_text_width_empty() {
3096        let text = "";
3097        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3098        assert!(
3099            (width - 0.0).abs() < f32::EPSILON,
3100            "Width should be 0 for empty string"
3101        );
3102    }
3103
3104    #[test]
3105    fn test_measure_text_width_emoji() {
3106        // "👋" (1 char, width > 1) -> FONT_SIZE
3107        let text = "👋";
3108        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3109        let expected = FONT_SIZE;
3110        assert_eq!(
3111            compare_floats(width, expected),
3112            CmpOrdering::Equal,
3113            "Width mismatch for emoji"
3114        );
3115    }
3116
3117    #[test]
3118    fn test_measure_text_width_korean() {
3119        // "안녕하세요" (5 chars)
3120        // Korean characters are typically full-width.
3121        // width = 5 * FONT_SIZE
3122        let text = "안녕하세요";
3123        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
3124        let expected = FONT_SIZE * 5.0;
3125        assert_eq!(
3126            compare_floats(width, expected),
3127            CmpOrdering::Equal,
3128            "Width mismatch for Korean"
3129        );
3130    }
3131
3132    #[test]
3133    fn test_measure_text_width_japanese() {
3134        // "こんにちは" (Hiragana, 5 chars) -> 5 * FONT_SIZE
3135        // "カタカナ" (Katakana, 4 chars) -> 4 * FONT_SIZE
3136        // "漢字" (Kanji, 2 chars) -> 2 * FONT_SIZE
3137
3138        let text_hiragana = "こんにちは";
3139        let width_hiragana =
3140            measure_text_width(text_hiragana, FONT_SIZE, CHAR_WIDTH);
3141        let expected_hiragana = FONT_SIZE * 5.0;
3142        assert_eq!(
3143            compare_floats(width_hiragana, expected_hiragana),
3144            CmpOrdering::Equal,
3145            "Width mismatch for Hiragana"
3146        );
3147
3148        let text_katakana = "カタカナ";
3149        let width_katakana =
3150            measure_text_width(text_katakana, FONT_SIZE, CHAR_WIDTH);
3151        let expected_katakana = FONT_SIZE * 4.0;
3152        assert_eq!(
3153            compare_floats(width_katakana, expected_katakana),
3154            CmpOrdering::Equal,
3155            "Width mismatch for Katakana"
3156        );
3157
3158        let text_kanji = "漢字";
3159        let width_kanji = measure_text_width(text_kanji, FONT_SIZE, CHAR_WIDTH);
3160        let expected_kanji = FONT_SIZE * 2.0;
3161        assert_eq!(
3162            compare_floats(width_kanji, expected_kanji),
3163            CmpOrdering::Equal,
3164            "Width mismatch for Kanji"
3165        );
3166    }
3167
3168    #[test]
3169    fn test_set_font_size() {
3170        let mut editor = CodeEditor::new("", "rs");
3171
3172        // Initial state (defaults)
3173        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
3174        assert!((editor.line_height() - 20.0).abs() < f32::EPSILON);
3175
3176        // Test auto adjust = true
3177        editor.set_font_size(28.0, true);
3178        assert!((editor.font_size() - 28.0).abs() < f32::EPSILON);
3179        // Line height should double: 20.0 * (28.0/14.0) = 40.0
3180        assert_eq!(
3181            compare_floats(editor.line_height(), 40.0),
3182            CmpOrdering::Equal
3183        );
3184
3185        // Test auto adjust = false
3186        // First set line height to something custom
3187        editor.set_line_height(50.0);
3188        // Change font size but keep line height
3189        editor.set_font_size(14.0, false);
3190        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
3191        // Line height should stay 50.0
3192        assert_eq!(
3193            compare_floats(editor.line_height(), 50.0),
3194            CmpOrdering::Equal
3195        );
3196        // Char width should have scaled back to roughly default (but depends on measurement)
3197        // We check if it is close to the expected value, but since measurement can vary,
3198        // we just ensure it is positive and close to what we expect (around 8.4)
3199        assert!(editor.char_width > 0.0);
3200        assert!((editor.char_width - CHAR_WIDTH).abs() < 0.5);
3201    }
3202
3203    #[test]
3204    fn test_measure_single_char_width() {
3205        let editor = CodeEditor::new("", "rs");
3206
3207        // Measure 'a'
3208        let width_a = editor.measure_single_char_width("a");
3209        assert!(width_a > 0.0, "Width of 'a' should be positive");
3210
3211        // Measure Chinese char
3212        let width_cjk = editor.measure_single_char_width("汉");
3213        assert!(width_cjk > 0.0, "Width of '汉' should be positive");
3214
3215        assert!(
3216            width_cjk > width_a,
3217            "Width of '汉' should be greater than 'a'"
3218        );
3219
3220        // Check that width_cjk is roughly double of width_a (common in terminal fonts)
3221        // but we just check it is significantly larger
3222        assert!(width_cjk >= width_a * 1.5);
3223    }
3224
3225    #[test]
3226    fn test_set_line_height() {
3227        let mut editor = CodeEditor::new("", "rs");
3228
3229        // Initial state
3230        assert!((editor.line_height() - LINE_HEIGHT).abs() < f32::EPSILON);
3231
3232        // Set custom line height
3233        editor.set_line_height(35.0);
3234        assert!((editor.line_height() - 35.0).abs() < f32::EPSILON);
3235
3236        // Font size should remain unchanged
3237        assert!((editor.font_size() - FONT_SIZE).abs() < f32::EPSILON);
3238    }
3239
3240    #[test]
3241    fn test_visual_lines_cached_reuses_cache_for_same_key() {
3242        let editor = CodeEditor::new("a\nb\nc", "rs");
3243
3244        let first = editor.visual_lines_cached(800.0);
3245        let second = editor.visual_lines_cached(800.0);
3246
3247        assert!(
3248            Rc::ptr_eq(&first, &second),
3249            "visual_lines_cached should reuse the cached Rc for identical keys"
3250        );
3251    }
3252
3253    #[derive(Default)]
3254    struct TestLspClient {
3255        changes: Rc<RefCell<Vec<Vec<lsp::LspTextChange>>>>,
3256    }
3257
3258    impl lsp::LspClient for TestLspClient {
3259        fn did_change(
3260            &mut self,
3261            _document: &lsp::LspDocument,
3262            changes: &[lsp::LspTextChange],
3263        ) {
3264            self.changes.borrow_mut().push(changes.to_vec());
3265        }
3266    }
3267
3268    #[test]
3269    fn test_word_start_in_line() {
3270        let line = "foo_bar baz";
3271        assert_eq!(CodeEditor::word_start_in_line(line, 0), 0);
3272        assert_eq!(CodeEditor::word_start_in_line(line, 2), 0);
3273        assert_eq!(CodeEditor::word_start_in_line(line, 4), 0);
3274        assert_eq!(CodeEditor::word_start_in_line(line, 7), 0);
3275        assert_eq!(CodeEditor::word_start_in_line(line, 9), 8);
3276    }
3277
3278    #[test]
3279    fn test_enqueue_lsp_change_auto_flush() {
3280        let changes = Rc::new(RefCell::new(Vec::new()));
3281        let client = TestLspClient { changes: Rc::clone(&changes) };
3282        let mut editor = CodeEditor::new("hello", "rs");
3283        editor.attach_lsp(
3284            Box::new(client),
3285            lsp::LspDocument::new("file:///test.rs", "rust"),
3286        );
3287        editor.set_lsp_auto_flush(true);
3288
3289        editor.buffer.insert_char(0, 5, '!');
3290        editor.enqueue_lsp_change();
3291
3292        let changes = changes.borrow();
3293        assert_eq!(changes.len(), 1);
3294        assert_eq!(changes[0].len(), 1);
3295        let change = &changes[0][0];
3296        assert_eq!(change.text, "!");
3297        assert_eq!(change.range.start.line, 0);
3298        assert_eq!(change.range.start.character, 5);
3299        assert_eq!(change.range.end.line, 0);
3300        assert_eq!(change.range.end.character, 5);
3301    }
3302
3303    #[test]
3304    fn test_editor_update_sends_bounded_incremental_lsp_change() {
3305        let changes = Rc::new(RefCell::new(Vec::new()));
3306        let client = TestLspClient { changes: Rc::clone(&changes) };
3307        let content = (0..10)
3308            .map(|line| format!("line{line}"))
3309            .collect::<Vec<_>>()
3310            .join("\n");
3311        let mut editor = CodeEditor::new(&content, "rs");
3312        editor.attach_lsp(
3313            Box::new(client),
3314            lsp::LspDocument::new("file:///large.rs", "rust"),
3315        );
3316        editor.request_focus();
3317        editor.has_canvas_focus = true;
3318        editor.focus_locked = false;
3319        editor.cursors.primary_mut().position = (5, 2);
3320
3321        let _ = editor.update(&Message::CharacterInput('X'));
3322
3323        let changes = changes.borrow();
3324        assert_eq!(changes.len(), 1);
3325        assert_eq!(changes[0].len(), 1);
3326        let change = &changes[0][0];
3327        assert_eq!(change.range.start.line, 4);
3328        assert_eq!(change.range.start.character, 0);
3329        assert_eq!(change.range.end.line, 7);
3330        assert_eq!(change.range.end.character, 0);
3331        assert_eq!(change.text, "line4\nliXne5\nline6\n");
3332        assert!(!editor.lsp_shadow_is_current);
3333        assert!(editor.lsp_shadow_text.is_empty());
3334    }
3335
3336    #[test]
3337    fn test_visual_lines_cached_changes_on_viewport_width_change() {
3338        let editor = CodeEditor::new("a\nb\nc", "rs");
3339
3340        let first = editor.visual_lines_cached(800.0);
3341        let second = editor.visual_lines_cached(801.0);
3342
3343        assert!(
3344            !Rc::ptr_eq(&first, &second),
3345            "visual_lines_cached should recompute when viewport width changes"
3346        );
3347    }
3348
3349    #[test]
3350    fn test_visual_lines_cached_changes_on_buffer_revision_change() {
3351        let mut editor = CodeEditor::new("a\nb\nc", "rs");
3352
3353        let first = editor.visual_lines_cached(800.0);
3354        editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
3355        let second = editor.visual_lines_cached(800.0);
3356
3357        assert!(
3358            !Rc::ptr_eq(&first, &second),
3359            "visual_lines_cached should recompute when buffer_revision changes"
3360        );
3361    }
3362
3363    #[test]
3364    fn test_max_content_width_increases_with_longer_lines() {
3365        let short = CodeEditor::new("ab", "rs");
3366        let long =
3367            CodeEditor::new("abcdefghijklmnopqrstuvwxyz0123456789", "rs");
3368
3369        assert!(
3370            long.max_content_width() > short.max_content_width(),
3371            "Longer lines should produce a greater max_content_width"
3372        );
3373    }
3374
3375    #[test]
3376    fn test_max_content_width_cached_by_revision() {
3377        let mut editor = CodeEditor::new("hello", "rs");
3378        let w1 = editor.max_content_width();
3379
3380        // Same revision → cache hit
3381        let w2 = editor.max_content_width();
3382        assert!(
3383            (w1 - w2).abs() < f32::EPSILON,
3384            "Repeated calls with same revision should return identical value"
3385        );
3386
3387        // Bump revision to simulate edit
3388        editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
3389        // Update the buffer to reflect a longer line
3390        editor.buffer = crate::text_buffer::TextBuffer::new(
3391            "hello world with extra content",
3392        );
3393        let w3 = editor.max_content_width();
3394        assert!(
3395            w3 > w1,
3396            "After revision bump with longer content, width should increase"
3397        );
3398    }
3399
3400    #[test]
3401    fn test_max_content_width_cache_updates_incrementally_after_newline() {
3402        let mut editor =
3403            CodeEditor::new("short\nthis is the longest line\ntail", "rs");
3404        editor.set_wrap_enabled(false);
3405        editor.request_focus();
3406        editor.has_canvas_focus = true;
3407        editor.focus_locked = false;
3408        editor.cursors.primary_mut().position = (1, 7);
3409        let _ = editor.max_content_width();
3410
3411        let _ = editor.update(&Message::Enter);
3412        let incremental = editor.max_content_width();
3413        let expected = CodeEditor::new(&editor.content(), "rs");
3414
3415        assert!(
3416            (incremental - expected.max_content_width()).abs() < f32::EPSILON
3417        );
3418        let cache = editor.max_content_width_cache.borrow();
3419        assert_eq!(
3420            cache.as_ref().map(|cache| cache.line_widths.len()),
3421            Some(editor.buffer.line_count())
3422        );
3423        assert_eq!(
3424            cache.as_ref().map(|cache| cache.revision),
3425            Some(editor.buffer_revision)
3426        );
3427    }
3428
3429    #[test]
3430    fn test_syntax_getter() {
3431        let editor = CodeEditor::new("", "lua");
3432        assert_eq!(editor.syntax(), "lua");
3433    }
3434
3435    /// Buffer with one outer block (lines 0..=4) and a nested inner block
3436    /// (lines 2..=3), used by the folding tests.
3437    fn folding_editor() -> CodeEditor {
3438        CodeEditor::new(
3439            "fn main() {\n    let x = 1;\n    if x > 0 {\n        print();\n    }\n}",
3440            "rs",
3441        )
3442    }
3443
3444    #[test]
3445    fn test_folding_enabled_by_default() {
3446        let editor = CodeEditor::new("fn main() {}", "rs");
3447        assert!(editor.folding_enabled());
3448    }
3449
3450    #[test]
3451    fn test_foldable_regions_detected() {
3452        let editor = folding_editor();
3453        let regions = editor.foldable_regions();
3454        assert_eq!(
3455            *regions,
3456            vec![
3457                folding::FoldRegion::new(0, 4),
3458                folding::FoldRegion::new(2, 3)
3459            ]
3460        );
3461    }
3462
3463    #[test]
3464    fn test_toggle_fold_hides_and_shows_lines() {
3465        let mut editor = folding_editor();
3466        let width = editor.viewport_width;
3467        let total = editor.visual_lines_cached(width).len();
3468
3469        editor.toggle_fold(0);
3470        assert!(editor.is_folded(0));
3471        // Outer block hides lines 1..=4: only lines 0 and 5 remain.
3472        assert_eq!(editor.visual_lines_cached(width).len(), 2);
3473
3474        editor.toggle_fold(0);
3475        assert!(!editor.is_folded(0));
3476        assert_eq!(editor.visual_lines_cached(width).len(), total);
3477    }
3478
3479    #[test]
3480    fn test_toggle_fold_ignores_non_header() {
3481        let mut editor = folding_editor();
3482        editor.toggle_fold(3); // line 3 is not a header
3483        assert!(!editor.is_folded(3));
3484        assert!(editor.collapsed_folds.is_empty());
3485    }
3486
3487    #[test]
3488    fn test_fold_at_picks_innermost_region() {
3489        let mut editor = folding_editor();
3490        // Line 3 is inside both (0,4) and (2,3); the innermost (header 2) folds.
3491        editor.fold_at(3);
3492        assert!(editor.is_folded(2));
3493        assert!(!editor.is_folded(0));
3494        assert_eq!(editor.hidden_lines_set(), [3].into_iter().collect());
3495    }
3496
3497    #[test]
3498    fn test_unfold_at_expands_innermost_region() {
3499        let mut editor = folding_editor();
3500        editor.fold_at(3);
3501        editor.unfold_at(2);
3502        assert!(!editor.is_folded(2));
3503        assert!(editor.hidden_lines_set().is_empty());
3504    }
3505
3506    #[test]
3507    fn test_toggle_fold_at_cursor_folds_then_unfolds() {
3508        let mut editor = folding_editor();
3509        // Line 3 is inside the innermost region (header 2).
3510        editor.toggle_fold_at(3);
3511        assert!(editor.is_folded(2));
3512
3513        // Toggling again on the same line expands it back.
3514        editor.toggle_fold_at(2);
3515        assert!(!editor.is_folded(2));
3516    }
3517
3518    #[test]
3519    fn test_toggle_fold_at_ignores_unfoldable_line() {
3520        let mut editor = CodeEditor::new("a\nb\nc", "rs");
3521        editor.toggle_fold_at(1);
3522        assert!(editor.collapsed_folds.is_empty());
3523    }
3524
3525    #[test]
3526    fn test_fold_all_and_unfold_all() {
3527        let mut editor = folding_editor();
3528        editor.fold_all();
3529        assert!(editor.is_folded(0));
3530        assert!(editor.is_folded(2));
3531        // Outer fold hides 1..=4, inner hides 3: union is 1..=4.
3532        assert_eq!(
3533            editor.hidden_lines_set(),
3534            [1, 2, 3, 4].into_iter().collect()
3535        );
3536
3537        editor.unfold_all();
3538        assert!(editor.collapsed_folds.is_empty());
3539        assert!(editor.hidden_lines_set().is_empty());
3540    }
3541
3542    #[test]
3543    fn test_fold_moves_cursor_out_of_hidden_lines() {
3544        let mut editor = folding_editor();
3545        editor.cursors.set_single((3, 2));
3546        editor.fold_all();
3547        // Line 3 is hidden; the cursor moves up to the nearest visible line (0).
3548        assert_eq!(editor.cursors.primary_position(), (0, 0));
3549    }
3550
3551    #[test]
3552    fn test_disabled_folding_yields_no_regions() {
3553        let mut editor = folding_editor();
3554        editor.set_folding_enabled(false);
3555        assert!(editor.foldable_regions().is_empty());
3556        // Collapsed state is preserved but produces no hidden lines while off.
3557        editor.collapsed_folds.insert(0);
3558        assert!(editor.hidden_lines_set().is_empty());
3559    }
3560}