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