use iced::advanced::text::{
Alignment, Paragraph, Renderer as TextRenderer, Text,
};
use iced::widget::operation::{RelativeOffset, snap_to};
use iced::widget::{Id, canvas};
use std::cell::{Cell, RefCell};
use std::cmp::Ordering as CmpOrdering;
use std::ops::Range;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use unicode_width::UnicodeWidthChar;
use crate::i18n::Translations;
use crate::text_buffer::TextBuffer;
use crate::theme::Style;
pub use history::CommandHistory;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
static EDITOR_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
static FOCUSED_EDITOR_ID: AtomicU64 = AtomicU64::new(0);
mod canvas_impl;
mod clipboard;
pub mod command;
mod cursor;
pub(crate) mod cursor_set;
pub mod history;
pub mod ime_requester;
pub mod lsp;
#[cfg(all(feature = "lsp-process", not(target_arch = "wasm32")))]
pub mod lsp_process;
mod search;
mod search_dialog;
mod selection;
mod update;
mod view;
mod wrapping;
pub(crate) const FONT_SIZE: f32 = 14.0;
pub(crate) const LINE_HEIGHT: f32 = 20.0;
pub(crate) const CHAR_WIDTH: f32 = 8.4; pub(crate) const TAB_WIDTH: usize = 4;
pub(crate) const GUTTER_WIDTH: f32 = 45.0;
pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
std::time::Duration::from_millis(530);
pub(crate) fn measure_char_width(
c: char,
full_char_width: f32,
char_width: f32,
) -> f32 {
if c == '\t' {
return char_width * TAB_WIDTH as f32;
}
match c.width() {
Some(w) if w > 1 => full_char_width,
Some(_) => char_width,
None => 0.0,
}
}
pub(crate) fn measure_text_width(
text: &str,
full_char_width: f32,
char_width: f32,
) -> f32 {
text.chars()
.map(|c| measure_char_width(c, full_char_width, char_width))
.sum()
}
pub(crate) const EPSILON: f32 = 0.001;
pub(crate) const CACHE_WINDOW_MARGIN_MULTIPLIER: usize = 2;
pub(crate) fn compare_floats(a: f32, b: f32) -> CmpOrdering {
if (a - b).abs() < EPSILON {
CmpOrdering::Equal
} else if a > b {
CmpOrdering::Greater
} else {
CmpOrdering::Less
}
}
#[derive(Debug, Clone)]
pub(crate) struct ImePreedit {
pub(crate) content: String,
pub(crate) selection: Option<Range<usize>>,
}
pub struct CodeEditor {
pub(crate) editor_id: u64,
pub(crate) buffer: TextBuffer,
pub(crate) cursors: cursor_set::CursorSet,
pub(crate) horizontal_scroll_offset: f32,
pub(crate) style: Style,
pub(crate) syntax: String,
pub(crate) last_blink: Instant,
pub(crate) cursor_visible: bool,
pub(crate) is_dragging: bool,
pub(crate) content_cache: canvas::Cache,
pub(crate) overlay_cache: canvas::Cache,
pub(crate) scrollable_id: Id,
pub(crate) horizontal_scrollable_id: Id,
pub(crate) max_content_width_cache: RefCell<Option<(u64, f32)>>,
pub(crate) viewport_scroll: f32,
pub(crate) viewport_height: f32,
pub(crate) viewport_width: f32,
pub(crate) history: CommandHistory,
pub(crate) is_grouping: bool,
pub(crate) wrap_enabled: bool,
pub(crate) auto_indent_enabled: bool,
pub(crate) indent_style: IndentStyle,
pub(crate) wrap_column: Option<usize>,
pub(crate) search_state: search::SearchState,
pub(crate) translations: Translations,
pub(crate) search_replace_enabled: bool,
pub(crate) line_numbers_enabled: bool,
pub(crate) lsp_enabled: bool,
pub(crate) lsp_client: Option<Box<dyn lsp::LspClient>>,
pub(crate) lsp_document: Option<lsp::LspDocument>,
pub(crate) lsp_pending_changes: Vec<lsp::LspTextChange>,
pub(crate) lsp_shadow_text: String,
pub(crate) lsp_auto_flush: bool,
pub(crate) has_canvas_focus: bool,
pub(crate) focus_locked: bool,
pub(crate) show_cursor: bool,
pub(crate) modifiers: Cell<iced::keyboard::Modifiers>,
pub(crate) font: iced::Font,
pub(crate) ime_preedit: Option<ImePreedit>,
pub(crate) font_size: f32,
pub(crate) full_char_width: f32,
pub(crate) line_height: f32,
pub(crate) char_width: f32,
pub(crate) last_first_visible_line: usize,
pub(crate) cache_window_start_line: usize,
pub(crate) cache_window_end_line: usize,
pub(crate) buffer_revision: u64,
visual_lines_cache: RefCell<Option<VisualLinesCache>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct VisualLinesKey {
buffer_revision: u64,
viewport_width_bits: u32,
gutter_width_bits: u32,
wrap_enabled: bool,
wrap_column: Option<usize>,
full_char_width_bits: u32,
char_width_bits: u32,
}
struct VisualLinesCache {
key: VisualLinesKey,
visual_lines: Rc<Vec<wrapping::VisualLine>>,
}
#[derive(Debug, Clone)]
pub enum Message {
CharacterInput(char),
Backspace,
Delete,
Enter,
Tab,
ArrowKey(ArrowDirection, bool),
MouseClick(iced::Point),
MouseDrag(iced::Point),
MouseHover(iced::Point),
MouseRelease,
Copy,
Paste(String),
DeleteSelection,
Tick,
PageUp,
PageDown,
Home(bool),
End(bool),
CtrlHome,
CtrlEnd,
GotoPosition(usize, usize),
Scrolled(iced::widget::scrollable::Viewport),
HorizontalScrolled(iced::widget::scrollable::Viewport),
Undo,
Redo,
OpenSearch,
OpenSearchReplace,
CloseSearch,
SearchQueryChanged(String),
ReplaceQueryChanged(String),
ToggleCaseSensitive,
FindNext,
FindPrevious,
ReplaceNext,
ReplaceAll,
SearchDialogTab,
SearchDialogShiftTab,
FocusNavigationTab,
FocusNavigationShiftTab,
CanvasFocusGained,
CanvasFocusLost,
JumpClick(iced::Point),
ImeOpened,
ImePreedit(String, Option<Range<usize>>),
ImeCommit(String),
ImeClosed,
AltClick(iced::Point),
AddCursorAbove,
AddCursorBelow,
SelectNextOccurrence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndentStyle {
Spaces(u8),
Tab,
}
impl IndentStyle {
pub const ALL: [IndentStyle; 4] = [
IndentStyle::Spaces(2),
IndentStyle::Spaces(4),
IndentStyle::Spaces(8),
IndentStyle::Tab,
];
}
impl std::fmt::Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndentStyle::Spaces(1) => write!(f, "1 space"),
IndentStyle::Spaces(n) => write!(f, "{n} spaces"),
IndentStyle::Tab => write!(f, "Tab"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum ArrowDirection {
Up,
Down,
Left,
Right,
}
impl CodeEditor {
pub fn new(content: &str, syntax: &str) -> Self {
let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
if editor_id == 1 {
FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
}
let mut editor = Self {
editor_id,
buffer: TextBuffer::new(content),
cursors: cursor_set::CursorSet::new((0, 0)),
horizontal_scroll_offset: 0.0,
style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
syntax: syntax.to_string(),
last_blink: Instant::now(),
cursor_visible: true,
is_dragging: false,
content_cache: canvas::Cache::default(),
overlay_cache: canvas::Cache::default(),
scrollable_id: Id::unique(),
horizontal_scrollable_id: Id::unique(),
max_content_width_cache: RefCell::new(None),
viewport_scroll: 0.0,
viewport_height: 600.0, viewport_width: 800.0, history: CommandHistory::new(100),
is_grouping: false,
wrap_enabled: true,
auto_indent_enabled: true,
indent_style: IndentStyle::Spaces(4),
wrap_column: None,
search_state: search::SearchState::new(),
translations: Translations::default(),
search_replace_enabled: true,
line_numbers_enabled: true,
lsp_enabled: true,
lsp_client: None,
lsp_document: None,
lsp_pending_changes: Vec::new(),
lsp_shadow_text: String::new(),
lsp_auto_flush: true,
has_canvas_focus: false,
focus_locked: false,
show_cursor: false,
modifiers: Cell::new(iced::keyboard::Modifiers::default()),
font: iced::Font::MONOSPACE,
ime_preedit: None,
font_size: FONT_SIZE,
full_char_width: CHAR_WIDTH * 2.0,
line_height: LINE_HEIGHT,
char_width: CHAR_WIDTH,
last_first_visible_line: 0,
cache_window_start_line: 0,
cache_window_end_line: 0,
buffer_revision: 0,
visual_lines_cache: RefCell::new(None),
};
editor.recalculate_char_dimensions(false);
editor
}
pub fn set_font(&mut self, font: iced::Font) {
self.font = font;
self.recalculate_char_dimensions(false);
}
pub fn set_font_size(&mut self, size: f32, auto_adjust_line_height: bool) {
self.font_size = size;
self.recalculate_char_dimensions(auto_adjust_line_height);
}
fn recalculate_char_dimensions(&mut self, auto_adjust_line_height: bool) {
self.char_width = self.measure_single_char_width("a");
self.full_char_width = self.measure_single_char_width("汉");
if self.char_width.is_infinite() {
self.char_width = self.font_size / 2.0; }
if self.full_char_width.is_infinite() {
self.full_char_width = self.font_size;
}
if auto_adjust_line_height {
let line_height_ratio = LINE_HEIGHT / FONT_SIZE;
self.line_height = self.font_size * line_height_ratio;
}
self.content_cache.clear();
self.overlay_cache.clear();
}
fn measure_single_char_width(&self, content: &str) -> f32 {
let text = Text {
content,
font: self.font,
size: iced::Pixels(self.font_size),
line_height: iced::advanced::text::LineHeight::default(),
bounds: iced::Size::new(f32::INFINITY, f32::INFINITY),
align_x: Alignment::Left,
align_y: iced::alignment::Vertical::Top,
shaping: iced::advanced::text::Shaping::Advanced,
wrapping: iced::advanced::text::Wrapping::default(),
};
let p = <iced::Renderer as TextRenderer>::Paragraph::with_text(text);
p.min_width()
}
pub fn font_size(&self) -> f32 {
self.font_size
}
pub fn char_width(&self) -> f32 {
self.char_width
}
pub fn full_char_width(&self) -> f32 {
self.full_char_width
}
pub fn measure_text_width(&self, text: &str) -> f32 {
measure_text_width(text, self.full_char_width, self.char_width)
}
pub fn set_line_height(&mut self, height: f32) {
self.line_height = height;
self.content_cache.clear();
self.overlay_cache.clear();
}
pub fn line_height(&self) -> f32 {
self.line_height
}
pub fn viewport_height(&self) -> f32 {
self.viewport_height
}
pub fn viewport_width(&self) -> f32 {
self.viewport_width
}
pub fn viewport_scroll(&self) -> f32 {
self.viewport_scroll
}
pub fn content(&self) -> String {
self.buffer.to_string()
}
#[must_use]
pub fn with_viewport_height(mut self, height: f32) -> Self {
self.viewport_height = height;
self
}
pub fn set_theme(&mut self, style: Style) {
self.style = style;
self.content_cache.clear();
self.overlay_cache.clear();
}
pub fn set_language(&mut self, language: crate::i18n::Language) {
self.translations.set_language(language);
self.overlay_cache.clear();
}
pub fn language(&self) -> crate::i18n::Language {
self.translations.language()
}
pub fn attach_lsp(
&mut self,
mut client: Box<dyn lsp::LspClient>,
mut document: lsp::LspDocument,
) {
if !self.lsp_enabled {
return;
}
document.version = 1;
let text = self.buffer.to_string();
client.did_open(&document, &text);
self.lsp_client = Some(client);
self.lsp_document = Some(document);
self.lsp_shadow_text = text;
self.lsp_pending_changes.clear();
}
pub fn lsp_open_document(&mut self, mut document: lsp::LspDocument) {
let Some(client) = self.lsp_client.as_mut() else { return };
if let Some(current) = self.lsp_document.as_ref() {
client.did_close(current);
}
document.version = 1;
let text = self.buffer.to_string();
client.did_open(&document, &text);
self.lsp_document = Some(document);
self.lsp_shadow_text = text;
self.lsp_pending_changes.clear();
}
pub fn detach_lsp(&mut self) {
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.did_close(document);
}
self.lsp_client = None;
self.lsp_document = None;
self.lsp_shadow_text = String::new();
self.lsp_pending_changes.clear();
}
pub fn lsp_did_save(&mut self) {
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
let text = self.buffer.to_string();
client.did_save(document, &text);
}
}
pub fn lsp_request_hover(&mut self) {
let position = self.lsp_position_from_cursor();
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_hover(document, position);
}
}
pub fn lsp_request_hover_at(&mut self, point: iced::Point) -> bool {
let Some(position) = self.lsp_position_from_point(point) else {
return false;
};
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_hover(document, position);
return true;
}
false
}
pub fn lsp_request_hover_at_position(
&mut self,
position: lsp::LspPosition,
) -> bool {
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_hover(document, position);
return true;
}
false
}
pub fn lsp_position_at_point(
&self,
point: iced::Point,
) -> Option<lsp::LspPosition> {
self.lsp_position_from_point(point)
}
pub fn lsp_hover_anchor_at_point(
&self,
point: iced::Point,
) -> Option<(lsp::LspPosition, iced::Point)> {
let (line, col) = self.calculate_cursor_from_point(point)?;
let line_content = self.buffer.line(line);
let anchor_col = Self::word_start_in_line(line_content, col);
let anchor_point =
self.point_from_position(line, anchor_col).unwrap_or(point);
let line = u32::try_from(line).unwrap_or(u32::MAX);
let character = u32::try_from(anchor_col).unwrap_or(u32::MAX);
Some((lsp::LspPosition { line, character }, anchor_point))
}
pub fn lsp_request_completion(&mut self) {
let position = self.lsp_position_from_cursor();
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_completion(document, position);
}
}
pub fn lsp_flush_pending_changes(&mut self) {
if self.lsp_pending_changes.is_empty() {
return;
}
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_mut())
{
let changes = std::mem::take(&mut self.lsp_pending_changes);
document.version = document.version.saturating_add(1);
client.did_change(document, &changes);
}
}
pub fn set_lsp_auto_flush(&mut self, auto_flush: bool) {
self.lsp_auto_flush = auto_flush;
}
pub fn request_focus(&self) {
FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
}
pub fn is_focused(&self) -> bool {
FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
}
pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
self.buffer = TextBuffer::new(content);
self.cursors.set_single((0, 0));
self.horizontal_scroll_offset = 0.0;
self.is_dragging = false;
self.viewport_scroll = 0.0;
self.history = CommandHistory::new(100);
self.is_grouping = false;
self.last_blink = Instant::now();
self.cursor_visible = true;
self.content_cache = canvas::Cache::default();
self.overlay_cache = canvas::Cache::default();
self.buffer_revision = self.buffer_revision.wrapping_add(1);
*self.visual_lines_cache.borrow_mut() = None;
self.enqueue_lsp_change();
snap_to(self.scrollable_id.clone(), RelativeOffset::START)
}
pub(crate) fn reset_cursor_blink(&mut self) {
self.last_blink = Instant::now();
self.cursor_visible = true;
}
fn lsp_position_from_cursor(&self) -> lsp::LspPosition {
let pos = self.cursors.primary_position();
let line = u32::try_from(pos.0).unwrap_or(u32::MAX);
let character = u32::try_from(pos.1).unwrap_or(u32::MAX);
lsp::LspPosition { line, character }
}
fn lsp_position_from_point(
&self,
point: iced::Point,
) -> Option<lsp::LspPosition> {
let (line, col) = self.calculate_cursor_from_point(point)?;
let line = u32::try_from(line).unwrap_or(u32::MAX);
let character = u32::try_from(col).unwrap_or(u32::MAX);
Some(lsp::LspPosition { line, character })
}
fn point_from_position(
&self,
line: usize,
col: usize,
) -> Option<iced::Point> {
let visual_lines = self.visual_lines_cached(self.viewport_width);
let visual_index = wrapping::WrappingCalculator::logical_to_visual(
&visual_lines,
line,
col,
)?;
let visual_line = &visual_lines[visual_index];
let line_content = self.buffer.line(visual_line.logical_line);
let prefix_len = col.saturating_sub(visual_line.start_col);
let prefix_text: String = line_content
.chars()
.skip(visual_line.start_col)
.take(prefix_len)
.collect();
let x = self.gutter_width()
+ 5.0
+ measure_text_width(
&prefix_text,
self.full_char_width,
self.char_width,
);
let y = visual_index as f32 * self.line_height;
Some(iced::Point::new(x, y))
}
pub(crate) fn word_start_in_line(line: &str, col: usize) -> usize {
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return 0;
}
let mut idx = col.min(chars.len());
if idx == chars.len() {
idx = idx.saturating_sub(1);
}
if !Self::is_word_char(chars[idx]) {
if idx > 0 && Self::is_word_char(chars[idx - 1]) {
idx -= 1;
} else {
return col.min(chars.len());
}
}
while idx > 0 && Self::is_word_char(chars[idx - 1]) {
idx -= 1;
}
idx
}
pub(crate) fn word_end_in_line(line: &str, col: usize) -> usize {
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return 0;
}
let mut idx = col.min(chars.len());
if idx == chars.len() {
idx = idx.saturating_sub(1);
}
if !Self::is_word_char(chars[idx]) {
if idx > 0 && Self::is_word_char(chars[idx - 1]) {
return idx;
} else {
return col.min(chars.len());
}
}
while idx < chars.len() && Self::is_word_char(chars[idx]) {
idx += 1;
}
idx
}
pub(crate) fn is_word_char(ch: char) -> bool {
ch == '_' || ch.is_alphanumeric()
}
fn enqueue_lsp_change(&mut self) {
if self.lsp_document.is_none() {
return;
}
let new_text = self.buffer.to_string();
let old_text = self.lsp_shadow_text.as_str();
if let Some(change) = lsp::compute_text_change(old_text, &new_text) {
self.lsp_pending_changes.push(change);
}
self.lsp_shadow_text = new_text;
if self.lsp_auto_flush {
self.lsp_flush_pending_changes();
}
}
pub(crate) fn refresh_search_matches_if_needed(&mut self) {
if self.search_state.is_open && !self.search_state.query.is_empty() {
self.search_state.update_matches(&self.buffer);
self.search_state
.select_match_near_cursor(self.cursors.primary_position());
}
}
pub fn is_modified(&self) -> bool {
self.history.is_modified()
}
pub fn mark_saved(&mut self) {
self.history.mark_saved();
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn set_wrap_enabled(&mut self, enabled: bool) {
if self.wrap_enabled != enabled {
self.wrap_enabled = enabled;
if enabled {
self.horizontal_scroll_offset = 0.0;
}
self.content_cache.clear();
self.overlay_cache.clear();
}
}
pub fn wrap_enabled(&self) -> bool {
self.wrap_enabled
}
pub fn set_auto_indent_enabled(&mut self, enabled: bool) {
self.auto_indent_enabled = enabled;
}
pub fn auto_indent_enabled(&self) -> bool {
self.auto_indent_enabled
}
pub fn set_indent_style(&mut self, style: IndentStyle) {
self.indent_style = style;
}
pub fn indent_style(&self) -> IndentStyle {
self.indent_style
}
pub fn set_search_replace_enabled(&mut self, enabled: bool) {
self.search_replace_enabled = enabled;
if !enabled && self.search_state.is_open {
self.search_state.close();
}
}
pub fn search_replace_enabled(&self) -> bool {
self.search_replace_enabled
}
pub fn set_lsp_enabled(&mut self, enabled: bool) {
self.lsp_enabled = enabled;
if !enabled {
self.detach_lsp();
}
}
pub fn lsp_enabled(&self) -> bool {
self.lsp_enabled
}
pub fn syntax(&self) -> &str {
&self.syntax
}
pub fn open_search_dialog(&mut self) -> iced::Task<Message> {
self.update(&Message::OpenSearch)
}
pub fn open_search_replace_dialog(&mut self) -> iced::Task<Message> {
self.update(&Message::OpenSearchReplace)
}
pub fn close_search_dialog(&mut self) -> iced::Task<Message> {
self.update(&Message::CloseSearch)
}
#[must_use]
pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
self.wrap_enabled = enabled;
self
}
#[must_use]
pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
self.wrap_column = column;
self
}
pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
if self.line_numbers_enabled != enabled {
self.line_numbers_enabled = enabled;
self.content_cache.clear();
self.overlay_cache.clear();
}
}
pub fn line_numbers_enabled(&self) -> bool {
self.line_numbers_enabled
}
#[must_use]
pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
self.line_numbers_enabled = enabled;
self
}
pub(crate) fn gutter_width(&self) -> f32 {
if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
}
pub fn lose_focus(&mut self) {
self.has_canvas_focus = false;
self.show_cursor = false;
self.ime_preedit = None;
}
pub fn reset_focus_lock(&mut self) {
self.focus_locked = false;
}
pub fn cursor_screen_position(&self) -> Option<iced::Point> {
let pos = self.cursors.primary_position();
self.point_from_position(pos.0, pos.1)
}
pub fn cursor_position(&self) -> (usize, usize) {
self.cursors.primary_position()
}
pub(crate) fn max_content_width(&self) -> f32 {
let mut cache = self.max_content_width_cache.borrow_mut();
if let Some((rev, w)) = *cache
&& rev == self.buffer_revision
{
return w;
}
let gutter = self.gutter_width();
let max_line_width = (0..self.buffer.line_count())
.map(|i| {
measure_text_width(
self.buffer.line(i),
self.full_char_width,
self.char_width,
)
})
.fold(0.0_f32, f32::max);
let total = gutter + 5.0 + max_line_width + 20.0;
*cache = Some((self.buffer_revision, total));
total
}
pub(crate) fn visual_lines_cached(
&self,
viewport_width: f32,
) -> Rc<Vec<wrapping::VisualLine>> {
let key = VisualLinesKey {
buffer_revision: self.buffer_revision,
viewport_width_bits: viewport_width.to_bits(),
gutter_width_bits: self.gutter_width().to_bits(),
wrap_enabled: self.wrap_enabled,
wrap_column: self.wrap_column,
full_char_width_bits: self.full_char_width.to_bits(),
char_width_bits: self.char_width.to_bits(),
};
let mut cache = self.visual_lines_cache.borrow_mut();
if let Some(existing) = cache.as_ref()
&& existing.key == key
{
return existing.visual_lines.clone();
}
let wrapping_calc = wrapping::WrappingCalculator::new(
self.wrap_enabled,
self.wrap_column,
self.full_char_width,
self.char_width,
);
let visual_lines = wrapping_calc.calculate_visual_lines(
&self.buffer,
viewport_width,
self.gutter_width(),
);
let visual_lines = Rc::new(visual_lines);
*cache =
Some(VisualLinesCache { key, visual_lines: visual_lines.clone() });
visual_lines
}
pub fn lsp_request_definition(&mut self) {
let position = self.lsp_position_from_cursor();
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_definition(document, position);
}
}
pub fn lsp_request_definition_at(&mut self, point: iced::Point) -> bool {
let Some(position) = self.lsp_position_from_point(point) else {
return false;
};
if let (Some(client), Some(document)) =
(self.lsp_client.as_mut(), self.lsp_document.as_ref())
{
client.request_definition(document, position);
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
#[test]
fn test_compare_floats() {
assert_eq!(
compare_floats(1.0, 1.0),
CmpOrdering::Equal,
"Exact equality"
);
assert_eq!(
compare_floats(1.0, 1.0 + 0.0001),
CmpOrdering::Equal,
"Within epsilon (positive)"
);
assert_eq!(
compare_floats(1.0, 1.0 - 0.0001),
CmpOrdering::Equal,
"Within epsilon (negative)"
);
assert_eq!(
compare_floats(1.0 + 0.002, 1.0),
CmpOrdering::Greater,
"Definitely greater"
);
assert_eq!(
compare_floats(1.0011, 1.0),
CmpOrdering::Greater,
"Just above epsilon"
);
assert_eq!(
compare_floats(1.0, 1.0 + 0.002),
CmpOrdering::Less,
"Definitely less"
);
assert_eq!(
compare_floats(1.0, 1.0011),
CmpOrdering::Less,
"Just below negative epsilon"
);
}
#[test]
fn test_measure_text_width_ascii() {
let text = "abc";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = CHAR_WIDTH * 3.0;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for ASCII"
);
}
#[test]
fn test_measure_text_width_cjk() {
let text = "你好";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = FONT_SIZE * 2.0;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for CJK"
);
}
#[test]
fn test_measure_text_width_mixed() {
let text = "Hi你好";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = CHAR_WIDTH * 2.0 + FONT_SIZE * 2.0;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for mixed content"
);
}
#[test]
fn test_measure_text_width_control_chars() {
let text = "\t\n";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = CHAR_WIDTH * TAB_WIDTH as f32;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for control chars"
);
}
#[test]
fn test_measure_text_width_empty() {
let text = "";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
assert!(
(width - 0.0).abs() < f32::EPSILON,
"Width should be 0 for empty string"
);
}
#[test]
fn test_measure_text_width_emoji() {
let text = "👋";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = FONT_SIZE;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for emoji"
);
}
#[test]
fn test_measure_text_width_korean() {
let text = "안녕하세요";
let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
let expected = FONT_SIZE * 5.0;
assert_eq!(
compare_floats(width, expected),
CmpOrdering::Equal,
"Width mismatch for Korean"
);
}
#[test]
fn test_measure_text_width_japanese() {
let text_hiragana = "こんにちは";
let width_hiragana =
measure_text_width(text_hiragana, FONT_SIZE, CHAR_WIDTH);
let expected_hiragana = FONT_SIZE * 5.0;
assert_eq!(
compare_floats(width_hiragana, expected_hiragana),
CmpOrdering::Equal,
"Width mismatch for Hiragana"
);
let text_katakana = "カタカナ";
let width_katakana =
measure_text_width(text_katakana, FONT_SIZE, CHAR_WIDTH);
let expected_katakana = FONT_SIZE * 4.0;
assert_eq!(
compare_floats(width_katakana, expected_katakana),
CmpOrdering::Equal,
"Width mismatch for Katakana"
);
let text_kanji = "漢字";
let width_kanji = measure_text_width(text_kanji, FONT_SIZE, CHAR_WIDTH);
let expected_kanji = FONT_SIZE * 2.0;
assert_eq!(
compare_floats(width_kanji, expected_kanji),
CmpOrdering::Equal,
"Width mismatch for Kanji"
);
}
#[test]
fn test_set_font_size() {
let mut editor = CodeEditor::new("", "rs");
assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
assert!((editor.line_height() - 20.0).abs() < f32::EPSILON);
editor.set_font_size(28.0, true);
assert!((editor.font_size() - 28.0).abs() < f32::EPSILON);
assert_eq!(
compare_floats(editor.line_height(), 40.0),
CmpOrdering::Equal
);
editor.set_line_height(50.0);
editor.set_font_size(14.0, false);
assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
assert_eq!(
compare_floats(editor.line_height(), 50.0),
CmpOrdering::Equal
);
assert!(editor.char_width > 0.0);
assert!((editor.char_width - CHAR_WIDTH).abs() < 0.5);
}
#[test]
fn test_measure_single_char_width() {
let editor = CodeEditor::new("", "rs");
let width_a = editor.measure_single_char_width("a");
assert!(width_a > 0.0, "Width of 'a' should be positive");
let width_cjk = editor.measure_single_char_width("汉");
assert!(width_cjk > 0.0, "Width of '汉' should be positive");
assert!(
width_cjk > width_a,
"Width of '汉' should be greater than 'a'"
);
assert!(width_cjk >= width_a * 1.5);
}
#[test]
fn test_set_line_height() {
let mut editor = CodeEditor::new("", "rs");
assert!((editor.line_height() - LINE_HEIGHT).abs() < f32::EPSILON);
editor.set_line_height(35.0);
assert!((editor.line_height() - 35.0).abs() < f32::EPSILON);
assert!((editor.font_size() - FONT_SIZE).abs() < f32::EPSILON);
}
#[test]
fn test_visual_lines_cached_reuses_cache_for_same_key() {
let editor = CodeEditor::new("a\nb\nc", "rs");
let first = editor.visual_lines_cached(800.0);
let second = editor.visual_lines_cached(800.0);
assert!(
Rc::ptr_eq(&first, &second),
"visual_lines_cached should reuse the cached Rc for identical keys"
);
}
#[derive(Default)]
struct TestLspClient {
changes: Rc<RefCell<Vec<Vec<lsp::LspTextChange>>>>,
}
impl lsp::LspClient for TestLspClient {
fn did_change(
&mut self,
_document: &lsp::LspDocument,
changes: &[lsp::LspTextChange],
) {
self.changes.borrow_mut().push(changes.to_vec());
}
}
#[test]
fn test_word_start_in_line() {
let line = "foo_bar baz";
assert_eq!(CodeEditor::word_start_in_line(line, 0), 0);
assert_eq!(CodeEditor::word_start_in_line(line, 2), 0);
assert_eq!(CodeEditor::word_start_in_line(line, 4), 0);
assert_eq!(CodeEditor::word_start_in_line(line, 7), 0);
assert_eq!(CodeEditor::word_start_in_line(line, 9), 8);
}
#[test]
fn test_enqueue_lsp_change_auto_flush() {
let changes = Rc::new(RefCell::new(Vec::new()));
let client = TestLspClient { changes: Rc::clone(&changes) };
let mut editor = CodeEditor::new("hello", "rs");
editor.attach_lsp(
Box::new(client),
lsp::LspDocument::new("file:///test.rs", "rust"),
);
editor.set_lsp_auto_flush(true);
editor.buffer.insert_char(0, 5, '!');
editor.enqueue_lsp_change();
let changes = changes.borrow();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].len(), 1);
let change = &changes[0][0];
assert_eq!(change.text, "!");
assert_eq!(change.range.start.line, 0);
assert_eq!(change.range.start.character, 5);
assert_eq!(change.range.end.line, 0);
assert_eq!(change.range.end.character, 5);
}
#[test]
fn test_visual_lines_cached_changes_on_viewport_width_change() {
let editor = CodeEditor::new("a\nb\nc", "rs");
let first = editor.visual_lines_cached(800.0);
let second = editor.visual_lines_cached(801.0);
assert!(
!Rc::ptr_eq(&first, &second),
"visual_lines_cached should recompute when viewport width changes"
);
}
#[test]
fn test_visual_lines_cached_changes_on_buffer_revision_change() {
let mut editor = CodeEditor::new("a\nb\nc", "rs");
let first = editor.visual_lines_cached(800.0);
editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
let second = editor.visual_lines_cached(800.0);
assert!(
!Rc::ptr_eq(&first, &second),
"visual_lines_cached should recompute when buffer_revision changes"
);
}
#[test]
fn test_max_content_width_increases_with_longer_lines() {
let short = CodeEditor::new("ab", "rs");
let long =
CodeEditor::new("abcdefghijklmnopqrstuvwxyz0123456789", "rs");
assert!(
long.max_content_width() > short.max_content_width(),
"Longer lines should produce a greater max_content_width"
);
}
#[test]
fn test_max_content_width_cached_by_revision() {
let mut editor = CodeEditor::new("hello", "rs");
let w1 = editor.max_content_width();
let w2 = editor.max_content_width();
assert!(
(w1 - w2).abs() < f32::EPSILON,
"Repeated calls with same revision should return identical value"
);
editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
editor.buffer = crate::text_buffer::TextBuffer::new(
"hello world with extra content",
);
let w3 = editor.max_content_width();
assert!(
w3 > w1,
"After revision bump with longer content, width should increase"
);
}
#[test]
fn test_syntax_getter() {
let editor = CodeEditor::new("", "lua");
assert_eq!(editor.syntax(), "lua");
}
}