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