Skip to main content

iced_code_editor/canvas_editor/
mod.rs

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