use super::buffer::{join_lines, normalize_line_endings, split_lines, EditorModel};
use super::grid::{LineGrid, RowLayout};
use super::multicursor::{self, MultiCursor};
use super::pairs::{self, PairAction};
use super::range_command::{TextRangeCommand, TypingAccum, TypingCommand};
use super::syntax::{BuiltinHighlighter, LanguageId, LineState, SyntaxHighlighter};
use super::types::{
CaretStyle, CodeEditorConfig, CompletionSource, CompletionState, ContextMenuState, Cursor,
CursorGoal, DiagnosticMarker, DocumentCompletions, DocumentScale, DocumentWordIndex,
EditRejection, EditorBuffer, ExtraSelection, FindState, FoldRegion, InlineDiagnostic,
LineEnding, MarkerSeverity, MenuItem, ReadOnlySpan, SearchMatch, SearchOptions, SyntaxPalette,
TextChange, TextPosition, TokenKind, TokenSpan, ViewportSnapshot, VisualLine, MAX_COMPLETIONS,
};
use crate::compat::MiniVec;
use crate::core::{Color, Font, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::signal::{GenericSignal, Signal1};
use crate::undo::{UndoStack, ValueCommand};
use crate::widget::capability::coercion::expect_string;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::text_utils::floor_char_boundary;
use crate::widget::{BaseWidget, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use alloc::boxed::Box;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
mod caret;
mod clipboard;
mod completion;
mod diagnostics;
mod document;
mod editing;
mod events;
mod find;
mod folding;
mod rows;
mod tabs;
mod tokens;
mod viewport;
mod widget_impl;
pub(crate) type SharedModel = Rc<RefCell<EditorModel>>;
struct CaretEdit {
start: TextPosition,
end: TextPosition,
insert: String,
caret_in_insert: Option<usize>,
}
impl CaretEdit {
fn new(start: TextPosition, end: TextPosition, insert: impl Into<String>) -> Self {
Self { start, end, insert: insert.into(), caret_in_insert: None }
}
fn with_caret(mut self, index: usize) -> Self {
self.caret_in_insert = Some(index);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct OccurrenceCache {
pub(crate) bounds: (TextPosition, TextPosition),
pub(crate) revision: u64,
pub(crate) matches: Vec<SearchMatch>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BracketCache {
pub(crate) revision: u64,
pub(crate) caret: TextPosition,
pub(crate) pair: Option<(TextPosition, TextPosition)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct Preedit {
pub(crate) text: String,
pub(crate) cursor: usize,
}
pub(crate) type DiagnosticRows = Option<(u64, Vec<(usize, usize)>)>;
#[derive(Debug, Clone, Default)]
pub(crate) struct BufferViewState {
pub(crate) cursor: Cursor,
pub(crate) extra_cursors: Vec<Cursor>,
pub(crate) goal: CursorGoal,
pub(crate) scroll_line: usize,
pub(crate) scroll_column: usize,
pub(crate) scroll_visual_row: usize,
pub(crate) folds: Vec<FoldRegion>,
}
pub struct CodeEditor {
pub(crate) base: BaseWidget,
pub(crate) model: SharedModel,
pub(crate) config: CodeEditorConfig,
highlighter: Option<Box<dyn SyntaxHighlighter>>,
language_highlighters: Vec<(LanguageId, Box<dyn SyntaxHighlighter>)>,
completion_source: Box<dyn CompletionSource>,
pub(crate) palette: SyntaxPalette,
pub(crate) cursor: Cursor,
pub(crate) extra_cursors: Vec<Cursor>,
pub(crate) goal: CursorGoal,
scroll_line: usize,
pub(crate) scroll_column: usize,
pub(crate) scroll_visual_row: usize,
pub(crate) visible_rows: usize,
pub(crate) measured_cell_width: core::cell::Cell<Option<f32>>,
pub(crate) measured_geometry: Option<Rect>,
pub(crate) find: FindState,
pub(crate) completion: CompletionState,
pub(crate) context_menu: ContextMenuState,
pub(crate) markers: Vec<DiagnosticMarker>,
pub(crate) hovered_diagnostic: Option<usize>,
clipboard_has_text: Option<bool>,
pub(crate) read_only_spans: Vec<ReadOnlySpan>,
folded_lines: MiniVec<usize>,
visual_row_prefix: Vec<usize>,
line_states: Vec<LineState>,
walked_lines: usize,
line_tokens: Vec<Option<Vec<TokenSpan>>>,
pub(crate) occurrence_cache: Option<OccurrenceCache>,
pub(crate) bracket_cache: RefCell<Option<BracketCache>>,
pub(crate) row_layouts: RefCell<Vec<(usize, RowLayout)>>,
pub(crate) segment_layouts: RefCell<Vec<((usize, usize), RowLayout)>>,
pub(crate) row_layout_key: core::cell::Cell<(
u64,
u64,
u32,
usize,
u32,
u32,
crate::widget::special_widgets::code_editor::syntax::LanguageId,
u64,
)>,
pub(crate) layout_lexer_generation: u64,
pub(crate) minimap_buckets: Vec<MinimapBucket>,
minimap_line_count: usize,
pub(crate) scale: DocumentScale,
pub(crate) minimap_dragging: bool,
pub(crate) preedit: Option<Preedit>,
pub(crate) tab_hits: RefCell<Vec<super::render::TabSlot>>,
pub(crate) hovered_tab: Option<usize>,
pub(crate) mouse_selecting: bool,
pub(crate) last_pointer_position: Option<Point>,
pub(crate) caret_blink: crate::style::CursorBlink,
undo_stack: UndoStack,
buffer_histories: Vec<UndoStack>,
buffer_views: Vec<BufferViewState>,
active_typing: Option<Rc<RefCell<TypingAccum>>>,
focused: bool,
pub(crate) caret_style: CaretStyle,
pub(crate) extra_selections: Vec<ExtraSelection>,
pub(crate) bookmarks: Vec<usize>,
pub(crate) breakpoints: Vec<usize>,
pub(crate) breakpoint_gutter: bool,
pub(crate) goto_bar_visible: bool,
pub(crate) goto_mode: bool,
pub(crate) overwrite: bool,
pub(crate) inline_suggestion: Option<String>,
pub(crate) popup_reveal: f32,
pub(crate) bracket_flash: f32,
pub(crate) fold_reveal: f32,
smooth_scroll_target: Option<usize>,
pub(crate) scrollbar_dragging: bool,
pub(crate) gutter_dragging: bool,
pub(crate) gutter_anchor: Option<usize>,
pub(crate) double_click_at: Option<Point>,
pub(crate) column_select_anchor: Option<TextPosition>,
pub(crate) column_selecting: bool,
pub(crate) dragging_text: bool,
pub(crate) drag_origin: Option<TextPosition>,
completion_index: Option<super::types::DocumentWordIndex>,
layout_generation: u64,
diagnostic_rows: RefCell<DiagnosticRows>,
#[cfg(test)]
pub(crate) diagnostic_cache_builds: core::cell::Cell<u32>,
damage: RefCell<Option<Rect>>,
bracket_index: Option<(u64, Vec<(TextPosition, TextPosition)>)>,
restoring_history: bool,
last_checkpoint_bytes: usize,
revision: u64,
pub text_changed: Signal1<String>,
pub text_change: Signal1<TextChange>,
pub marker_clicked: Signal1<usize>,
pub cursor_moved: Signal1<(usize, usize)>,
pub selection_changed: Signal1<Option<Rect>>,
pub tab_changed: Signal1<usize>,
pub search_changed: Signal1<usize>,
pub fold_changed: Signal1<usize>,
pub completion_changed: Signal1<bool>,
pub search_result: Signal1<(usize, usize, usize, usize)>,
pub zoom_changed: Signal1<f32>,
pub breakpoint_toggled: Signal1<(usize, bool)>,
pub marker_activated: Signal1<usize>,
pub palette_changed: GenericSignal,
}
impl CodeEditor {
pub fn new(geometry: Rect) -> Self {
let config = CodeEditorConfig::default();
debug_assert!(config.validate().is_ok(), "default configuration must be valid");
Self::build(geometry, config)
}
pub fn with_config(geometry: Rect, config: CodeEditorConfig) -> Result<Self, &'static str> {
config.validate()?;
Ok(Self::build(geometry, config))
}
fn build(geometry: Rect, config: CodeEditorConfig) -> Self {
let undo_limit = config.undo_stack_limit;
let mut editor = Self {
base: BaseWidget::new(WidgetKind::RichEdit, geometry, "CodeEditor"),
model: Rc::new(RefCell::new(EditorModel::new())),
config,
highlighter: None,
language_highlighters: Vec::new(),
completion_source: Box::new(DocumentCompletions),
palette: SyntaxPalette::for_active_appearance(),
cursor: Cursor::default(),
extra_cursors: Vec::new(),
goal: CursorGoal::default(),
scroll_line: 0,
scroll_column: 0,
scroll_visual_row: 0,
visible_rows: 0,
measured_cell_width: core::cell::Cell::new(None),
measured_geometry: None,
find: FindState::default(),
completion: CompletionState::default(),
context_menu: ContextMenuState::default(),
markers: Vec::new(),
hovered_diagnostic: None,
clipboard_has_text: None,
read_only_spans: Vec::new(),
folded_lines: MiniVec::new(),
visual_row_prefix: Vec::new(),
line_states: Vec::new(),
line_tokens: Vec::new(),
occurrence_cache: None,
bracket_cache: RefCell::new(None),
row_layouts: RefCell::new(Vec::new()),
segment_layouts: RefCell::new(Vec::new()),
row_layout_key: core::cell::Cell::new((
0,
0,
0,
0,
0,
0,
crate::widget::special_widgets::code_editor::syntax::LanguageId::default(),
0,
)),
layout_lexer_generation: 0,
walked_lines: 0,
minimap_buckets: Vec::new(),
minimap_line_count: 0,
scale: DocumentScale::Full,
mouse_selecting: false,
minimap_dragging: false,
preedit: None,
tab_hits: RefCell::new(Vec::new()),
hovered_tab: None,
last_pointer_position: None,
caret_blink: crate::style::CursorBlink::new(),
undo_stack: UndoStack::with_capacity(undo_limit),
buffer_histories: Vec::new(),
buffer_views: vec![BufferViewState::default()],
active_typing: None,
focused: true,
caret_style: CaretStyle::Line,
extra_selections: Vec::new(),
bookmarks: Vec::new(),
breakpoints: Vec::new(),
breakpoint_gutter: false,
goto_bar_visible: false,
goto_mode: false,
overwrite: false,
inline_suggestion: None,
popup_reveal: 1.0,
bracket_flash: 0.0,
fold_reveal: 1.0,
smooth_scroll_target: None,
scrollbar_dragging: false,
gutter_dragging: false,
gutter_anchor: None,
double_click_at: None,
column_select_anchor: None,
column_selecting: false,
dragging_text: false,
drag_origin: None,
completion_index: None,
layout_generation: 0,
diagnostic_rows: RefCell::new(None),
#[cfg(test)]
diagnostic_cache_builds: core::cell::Cell::new(0),
damage: RefCell::new(None),
bracket_index: None,
restoring_history: false,
last_checkpoint_bytes: 0,
revision: 0,
text_changed: Signal1::new(),
text_change: Signal1::new(),
marker_clicked: Signal1::new(),
cursor_moved: Signal1::new(),
selection_changed: Signal1::new(),
tab_changed: Signal1::new(),
search_changed: Signal1::new(),
fold_changed: Signal1::new(),
completion_changed: Signal1::new(),
search_result: Signal1::new(),
zoom_changed: Signal1::new(),
breakpoint_toggled: Signal1::new(),
marker_activated: Signal1::new(),
palette_changed: GenericSignal::new(),
};
if !editor.config.read_only {
editor.caret_blink.start();
}
editor.refresh_derived_state();
editor
}
pub fn config(&self) -> &CodeEditorConfig {
&self.config
}
pub fn is_read_only(&self) -> bool {
!self.is_editable()
}
pub fn set_read_only(&mut self, read_only: bool) {
self.config.read_only = read_only;
if read_only {
self.caret_blink.stop();
} else {
self.caret_blink.start();
}
self.base.request_redraw();
}
pub fn is_focused(&self) -> bool {
self.focused
}
pub fn set_focused(&mut self, focused: bool) {
if self.focused == focused {
return;
}
self.focused = focused;
if focused && self.is_editable() {
self.caret_blink.start();
} else {
self.caret_blink.stop();
}
self.base.request_redraw();
}
pub fn caret_style(&self) -> CaretStyle {
self.caret_style
}
pub fn set_caret_style(&mut self, style: CaretStyle) {
if self.caret_style == style {
return;
}
self.caret_style = style;
self.base.request_redraw();
}
pub fn overwrite_mode(&self) -> bool {
self.overwrite
}
pub fn set_overwrite_mode(&mut self, overwrite: bool) {
if self.overwrite == overwrite {
return;
}
self.overwrite = overwrite;
self.base.request_redraw();
}
pub fn toggle_overwrite_mode(&mut self) -> bool {
self.overwrite = !self.overwrite;
self.base.request_redraw();
self.overwrite
}
pub fn inline_suggestion(&self) -> Option<&str> {
self.inline_suggestion.as_deref()
}
pub fn set_inline_suggestion(&mut self, suggestion: Option<String>) {
if self.inline_suggestion == suggestion {
return;
}
self.inline_suggestion = suggestion;
self.base.request_redraw();
}
pub fn extra_selections(&self) -> &[ExtraSelection] {
&self.extra_selections
}
pub fn set_extra_selections(&mut self, selections: Vec<ExtraSelection>) {
if self.extra_selections == selections {
return;
}
self.extra_selections = selections;
self.base.request_redraw();
}
pub fn add_extra_selection(&mut self, selection: ExtraSelection) {
if selection.start >= selection.end {
return;
}
self.extra_selections.push(selection);
self.base.request_redraw();
}
pub fn clear_extra_selections(&mut self) {
if self.extra_selections.is_empty() {
return;
}
self.extra_selections.clear();
self.base.request_redraw();
}
pub fn bookmarks(&self) -> &[usize] {
&self.bookmarks
}
pub fn toggle_bookmark(&mut self, line: usize) -> bool {
let line = line.min(self.line_count().saturating_sub(1));
let bookmarked = if let Some(index) = self.bookmarks.iter().position(|entry| *entry == line)
{
self.bookmarks.remove(index);
false
} else {
self.bookmarks.push(line);
self.bookmarks.sort_unstable();
true
};
self.base.request_redraw();
bookmarked
}
pub fn goto_next_bookmark(&mut self) -> bool {
let Some(target) = self
.bookmarks
.iter()
.find(|line| **line > self.cursor.head.line)
.or_else(|| self.bookmarks.first())
.copied()
else {
return false;
};
self.move_caret_to(TextPosition::new(target, 0), false);
true
}
pub fn undo_redo_enabled(&self) -> bool {
self.config.undo_redo_enabled
}
pub fn set_undo_redo_enabled(&mut self, enabled: bool) {
if self.config.undo_redo_enabled == enabled {
return;
}
self.config.undo_redo_enabled = enabled;
}
pub fn undo_depth(&self) -> usize {
self.undo_stack.undo_count()
}
pub fn set_undo_limit(&mut self, limit: usize) {
self.config.undo_stack_limit = limit;
self.undo_stack.set_max_capacity(limit);
for history in &mut self.buffer_histories {
history.set_max_capacity(limit);
}
}
pub fn breakpoints(&self) -> &[usize] {
&self.breakpoints
}
pub fn is_breakpoint_gutter(&self) -> bool {
self.breakpoint_gutter
}
pub fn set_breakpoint_gutter(&mut self, visible: bool) {
if self.breakpoint_gutter == visible {
return;
}
self.breakpoint_gutter = visible;
self.base.request_layout();
self.base.request_redraw();
}
pub fn is_breakpoint(&self, line: usize) -> bool {
self.breakpoints.binary_search(&line).is_ok()
}
pub fn add_breakpoint(&mut self, line: usize) -> bool {
let line = line.min(self.line_count().saturating_sub(1));
if self.is_breakpoint(line) {
return false;
}
let index = self.breakpoints.partition_point(|entry| *entry < line);
self.breakpoints.insert(index, line);
self.breakpoint_toggled.emit((line, true));
self.base.request_redraw();
true
}
pub fn remove_breakpoint(&mut self, line: usize) -> bool {
let Ok(index) = self.breakpoints.binary_search(&line) else { return false };
self.breakpoints.remove(index);
self.breakpoint_toggled.emit((line, false));
self.base.request_redraw();
true
}
pub fn toggle_breakpoint(&mut self, line: usize) -> bool {
if self.is_breakpoint(line.min(self.line_count().saturating_sub(1))) {
self.remove_breakpoint(line.min(self.line_count().saturating_sub(1)));
false
} else {
self.add_breakpoint(line);
true
}
}
pub fn zoom_in(&mut self) {
self.apply_zoom(self.config.font_size + 1.0);
}
pub fn zoom_out(&mut self) {
self.apply_zoom(self.config.font_size - 1.0);
}
pub fn reset_zoom(&mut self) {
self.apply_zoom(super::types::DEFAULT_FONT_SIZE);
}
fn apply_zoom(&mut self, size: f32) {
let size = size.clamp(6.0, 48.0);
if (size - self.config.font_size).abs() < f32::EPSILON {
return;
}
self.config.font_size = size;
self.config.line_advance = 0.0;
self.measured_geometry = None;
self.refresh_visible_rows();
self.base.request_layout();
self.base.request_redraw();
self.zoom_changed.emit(size);
}
pub fn preedit_screen_origin(&self) -> Option<Point> {
self.preedit.as_ref()?;
let map = self.coordinate_map();
let row = self.caret_visual_row();
Some(Point::new(self.caret_x(self.cursor.head), map.row_y(row)))
}
pub fn set_placeholder_text(&mut self, text: Option<String>) {
if self.config.placeholder_text == text {
return;
}
self.config.placeholder_text = text;
self.base.request_redraw();
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
let mut running = false;
let mut caret_changed = false;
if self.is_editable() && self.focused {
let was_visible = self.caret_blink.is_visible();
running = self.caret_blink.tick(delta_ms);
caret_changed = was_visible != self.caret_blink.is_visible();
}
if self.config.fade_popups
&& (self.completion.visible || self.context_menu.visible)
&& self.popup_reveal < 1.0
{
self.popup_reveal = (self.popup_reveal + delta_ms as f32 / 120.0).min(1.0);
running = true;
}
if self.config.animate_brackets && self.bracket_flash > 0.0 {
self.bracket_flash = (self.bracket_flash - delta_ms as f32 / 180.0).max(0.0);
running = true;
}
if self.config.animate_folds && self.fold_reveal < 1.0 {
self.fold_reveal = (self.fold_reveal + delta_ms as f32 / 150.0).min(1.0);
running = true;
}
if self.smooth_scroll_target.is_some() {
running = true;
self.step_smooth_scroll();
}
if caret_changed {
self.mark_caret_damage();
}
if running {
self.base.request_redraw();
}
running
}
fn mark_caret_damage(&mut self) {
let row = self.caret_visual_row();
let y = self.coordinate_map().row_y(row);
let x = self.caret_x(self.cursor.head);
*self.damage.borrow_mut() =
Some(Rect::new(x.saturating_sub(1), y, 4, self.line_height().ceil().max(1.0) as u32));
}
pub fn take_damage_rect(&mut self) -> Option<Rect> {
self.damage.borrow_mut().take()
}
fn step_smooth_scroll(&mut self) {
let Some(target) = self.smooth_scroll_target else { return };
let current = self.scroll_visual_row;
let step = 4usize;
let next = if target > current {
(current + step).min(target)
} else {
current.saturating_sub(step).max(target)
};
if next != current {
self.scroll_visual_row = next;
self.scroll_line = self.visual_row_to_line(next);
self.base.request_redraw();
}
if next == target {
self.smooth_scroll_target = None;
}
}
pub fn is_caret_visible(&self) -> bool {
self.caret_blink.is_visible()
}
pub fn tab_width(&self) -> usize {
self.config.tab_width
}
pub fn set_tab_width(&mut self, width: usize) {
if width == 0 || width > 16 {
return;
}
self.config.tab_width = width;
self.base.request_layout();
self.base.request_redraw();
}
pub fn font(&self) -> Font {
Font::simple(self.config.font_family.clone(), self.config.font_size)
}
pub fn palette(&self) -> &SyntaxPalette {
&self.palette
}
pub fn set_palette(&mut self, palette: SyntaxPalette) {
self.palette = palette;
self.base.request_redraw();
}
pub fn set_highlighter(&mut self, highlighter: Box<dyn SyntaxHighlighter>) {
self.highlighter = Some(highlighter);
self.invalidate_line_states();
self.base.request_redraw();
}
pub fn clear_highlighter(&mut self) {
self.highlighter = None;
self.invalidate_line_states();
self.base.request_redraw();
}
pub fn register_language_highlighter(
&mut self,
language: LanguageId,
highlighter: Box<dyn SyntaxHighlighter>,
) {
match self.language_highlighters.iter_mut().find(|(registered, _)| *registered == language)
{
Some(slot) => slot.1 = highlighter,
None => self.language_highlighters.push((language, highlighter)),
}
if self.active_language() == language {
self.invalidate_line_states();
}
self.base.request_redraw();
}
pub fn unregister_language_highlighter(&mut self, language: LanguageId) -> bool {
let before = self.language_highlighters.len();
self.language_highlighters.retain(|(registered, _)| *registered != language);
let removed = self.language_highlighters.len() != before;
if removed && self.active_language() == language {
self.invalidate_line_states();
self.base.request_redraw();
}
removed
}
pub fn registered_languages(&self) -> Vec<LanguageId> {
self.language_highlighters.iter().map(|(language, _)| *language).collect()
}
pub fn highlighter_name_for(&self, language: LanguageId) -> String {
if let Some((_, highlighter)) =
self.language_highlighters.iter().find(|(registered, _)| *registered == language)
{
return highlighter.language_name().to_string();
}
if let Some(highlighter) = &self.highlighter {
return highlighter.language_name().to_string();
}
language.name().to_string()
}
pub fn set_completion_source(&mut self, source: Box<dyn CompletionSource>) {
self.completion_source = source;
}
pub fn document_scale(&self) -> DocumentScale {
self.scale
}
pub fn is_editable(&self) -> bool {
!self.config.read_only && self.scale.allows_editing()
}
pub fn read_only_spans(&self) -> &[ReadOnlySpan] {
&self.read_only_spans
}
pub fn set_read_only_spans(&mut self, spans: Vec<ReadOnlySpan>) {
self.read_only_spans = spans.into_iter().filter(|span| !span.is_empty()).collect();
self.base.request_redraw();
}
pub fn add_read_only_span(&mut self, span: ReadOnlySpan) {
if !span.is_empty() {
self.read_only_spans.push(span);
self.base.request_redraw();
}
}
pub fn clear_read_only_spans(&mut self) {
if !self.read_only_spans.is_empty() {
self.read_only_spans.clear();
self.base.request_redraw();
}
}
pub fn is_position_editable(&self, position: TextPosition) -> bool {
self.is_editable() && !self.read_only_spans.iter().any(|span| span.contains(position))
}
pub fn is_range_editable(&self, start: TextPosition, end: TextPosition) -> bool {
self.is_editable() && !self.read_only_spans.iter().any(|span| span.overlaps(start, end))
}
pub fn shift_read_only_spans_after(&mut self, first_line: usize, lines_delta: isize) {
if self.read_only_spans.is_empty() || lines_delta == 0 {
return;
}
for span in &mut self.read_only_spans {
if span.start_line >= first_line {
span.start_line = shift_index(span.start_line, lines_delta);
}
if span.end_line >= first_line {
span.end_line = shift_index(span.end_line, lines_delta);
}
}
self.read_only_spans.retain(|span| !span.is_empty());
self.base.request_redraw();
}
fn shift_read_only_spans_for_edit(
&mut self,
first_line: usize,
lines_now: usize,
lines_before: usize,
) {
let delta = lines_now as isize - lines_before as isize;
if delta == 0 || self.read_only_spans.is_empty() {
return;
}
let edit_end = first_line + lines_before;
for span in &mut self.read_only_spans {
if span.start_line >= edit_end {
span.start_line = shift_index(span.start_line, delta);
span.end_line = shift_index(span.end_line, delta);
} else if span.end_line >= edit_end {
span.end_line = shift_index(span.end_line, delta);
}
}
self.read_only_spans.retain(|span| !span.is_empty());
}
pub fn language_name(&self) -> String {
self.highlighter_name_for(self.active_language())
}
pub fn active_language(&self) -> LanguageId {
self.model
.borrow()
.all_buffers
.get(self.model.borrow().active_tab)
.and_then(|buffer| buffer.language)
.unwrap_or(self.config.language)
}
pub fn set_language(&mut self, language: LanguageId) {
self.config.language = language;
self.config.tab_width = language.default_tab_width();
let active = self.model.borrow().active_tab;
if let Some(buffer) = self.model.borrow_mut().all_buffers.get_mut(active) {
buffer.language = Some(language);
}
self.invalidate_line_states();
self.base.request_redraw();
}
pub fn set_buffer_language(&mut self, index: usize, language: Option<LanguageId>) -> bool {
let active = self.model.borrow().active_tab;
let changed = {
let mut model = self.model.borrow_mut();
match model.all_buffers.get_mut(index) {
Some(buffer) => {
buffer.language = language;
true
}
None => false,
}
};
if changed && index == active {
self.invalidate_line_states();
self.base.request_redraw();
}
changed
}
pub fn buffer_language(&self, index: usize) -> Option<LanguageId> {
self.model.borrow().all_buffers.get(index).and_then(|buffer| buffer.language)
}
pub fn keywords(&self) -> &'static [&'static str] {
self.active_language().keywords()
}
pub(crate) const MODIFIER_CONTROL: u32 = 2;
}
fn closes_bracket(ch: char, candidate: char) -> bool {
matches!((ch, candidate), ('(', ')') | ('[', ']') | ('{', '}'))
}
fn opens_bracket(ch: char, candidate: char) -> bool {
matches!((ch, candidate), (')', '(') | (']', '[') | ('}', '{'))
}
fn is_skipped(byte: usize, spans: &[(usize, usize)]) -> bool {
spans.iter().any(|(start, end)| byte >= *start && byte < *end)
}
fn byte_and_char_at(text: &str, column: usize) -> Option<(usize, char)> {
text.char_indices().nth(column)
}
pub(crate) fn decimal_digits(value: usize) -> usize {
let mut digits = 1usize;
let mut rest = value;
while rest >= 10 {
rest /= 10;
digits += 1;
}
digits
}
fn shift_index(index: usize, delta: isize) -> usize {
if delta >= 0 {
index.saturating_add(delta as usize)
} else {
index.saturating_sub(delta.unsigned_abs())
}
}
fn position_in_text(text: &str, offset: usize) -> TextPosition {
let offset = floor_char_boundary(text, offset.min(text.len()));
let mut line = 0usize;
let mut column = 0usize;
for ch in text[..offset].chars() {
if ch == '\n' {
line += 1;
column = 0;
} else {
column += 1;
}
}
TextPosition::new(line, column)
}
fn char_len_of(text: &str) -> usize {
text.chars().count()
}
fn trailing_line_len(payload: &str) -> usize {
payload.rsplit('\n').next().map(|line| line.chars().count()).unwrap_or(0)
}
fn whole_word_ok(
haystack: &[char],
start: usize,
length: usize,
options: SearchOptions,
language: LanguageId,
) -> bool {
if !options.whole_word {
return true;
}
let before_ok = start == 0 || !language.is_word_char(haystack[start - 1]);
let after = start + length;
let after_ok = after >= haystack.len() || !language.is_word_char(haystack[after]);
before_ok && after_ok
}
pub(crate) const MAX_MINIMAP_ROWS: usize = 256;
struct Checkpoint {
range: Option<(usize, usize, String)>,
removed: Option<String>,
before: Option<String>,
}
fn diff_range(before: &str, after: &str) -> Option<(usize, usize, String)> {
if before == after {
return None;
}
let before_bytes = before.as_bytes();
let after_bytes = after.as_bytes();
let max_prefix = before_bytes.len().min(after_bytes.len());
let mut prefix = 0usize;
while prefix < max_prefix && before_bytes[prefix] == after_bytes[prefix] {
prefix += 1;
}
while prefix > 0 && !after.is_char_boundary(prefix) {
prefix -= 1;
}
let max_suffix = (before_bytes.len() - prefix).min(after_bytes.len() - prefix);
let mut suffix = 0usize;
while suffix < max_suffix
&& before_bytes[before_bytes.len() - 1 - suffix]
== after_bytes[after_bytes.len() - 1 - suffix]
{
suffix += 1;
}
while suffix > 0 && !after.is_char_boundary(after.len() - suffix) {
suffix -= 1;
}
while suffix > 0 && !before.is_char_boundary(before.len() - suffix) {
suffix -= 1;
}
let removed_len = before_bytes.len().saturating_sub(prefix + suffix);
let inserted_end = after_bytes.len().saturating_sub(suffix);
Some((prefix, removed_len, after[prefix..inserted_end].to_string()))
}
fn clamp_and_merge(text: &str, raw: Vec<TokenSpan>) -> Vec<TokenSpan> {
let length = text.len();
let mut spans: Vec<TokenSpan> = Vec::with_capacity(raw.len());
for mut span in raw {
span.start = floor_char_boundary(text, span.start.min(length));
span.end = floor_char_boundary(text, span.end.min(length));
if span.end > span.start {
spans.push(span);
}
}
spans.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| a.end.cmp(&b.end)));
super::syntax::merge_adjacent(spans)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum MiniTone {
#[default]
Plain,
Comment,
String,
}
pub(crate) fn common_leading_whitespace(text: &str) -> String {
let mut common: Option<&str> = None;
for line in text.split('\n') {
let line = line.trim_end_matches(['\r']);
if line.trim().is_empty() {
continue;
}
let indent = &line[..line.len() - line.trim_start_matches([' ', '\t']).len()];
common = Some(match common {
None => indent,
Some(previous) => {
let shared = previous
.bytes()
.zip(indent.bytes())
.take_while(|(left, right)| left == right)
.count();
&previous[..shared]
}
});
}
common.unwrap_or("").to_string()
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct MinimapBucket {
pub(crate) indent: usize,
pub(crate) length: usize,
pub(crate) tone: MiniTone,
pub(crate) blank: bool,
}
fn minimap_partition(count: usize, rows: usize) -> (usize, usize) {
let per_bucket = count.div_ceil(rows.max(1)).max(1);
let buckets = count.div_ceil(per_bucket).max(1);
(buckets, per_bucket)
}
impl Default for MinimapBucket {
fn default() -> Self {
Self { indent: 0, length: 0, tone: MiniTone::Plain, blank: true }
}
}
impl MinimapBucket {
fn accumulate(slot: &mut Self, text: &str) {
let indent = text.chars().take_while(|ch| ch.is_whitespace()).count();
let length = text.trim_end().chars().count().saturating_sub(indent);
if length == 0 {
return;
}
if slot.blank || length > slot.length {
slot.indent = indent;
slot.length = length;
let trimmed = text.trim_start();
slot.tone = if trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with('*')
|| trimmed.starts_with('#')
{
MiniTone::Comment
} else if trimmed.contains('"') || trimmed.contains('\'') {
MiniTone::String
} else {
MiniTone::Plain
};
}
slot.blank = false;
}
}
#[cfg(test)]
mod diff_range_tests {
use super::diff_range;
#[test]
fn a_single_substitution_is_one_range() {
assert_eq!(diff_range("a-b-c", "a+b+c"), Some((1, 3, "+b+".to_string())));
}
#[test]
fn pure_insertion_and_deletion() {
assert_eq!(diff_range("ac", "abc"), Some((1, 0, "b".to_string())));
assert_eq!(diff_range("abc", "ac"), Some((1, 1, String::new())));
}
#[test]
fn identical_texts_produce_no_edit() {
assert_eq!(diff_range("same", "same"), None);
assert_eq!(diff_range("", ""), None);
}
#[test]
fn multibyte_boundaries_are_respected() {
let (start, removed, inserted) = diff_range("héllo", "hello").unwrap();
assert!("héllo".is_char_boundary(start));
assert!("héllo".is_char_boundary(start + removed));
assert_eq!(inserted, "e");
}
#[test]
fn replacing_the_whole_text() {
assert_eq!(diff_range("abc", "xyz"), Some((0, 3, "xyz".to_string())));
}
}