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