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