Files
lime/crates/lime-ui/src/editor_view.rs
T
2026-06-21 20:08:56 -05:00

207 lines
6.0 KiB
Rust

use lime_core::{Editor, Position};
use lime_syntax::{HighlightKind, HighlightSpan};
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::config::Config;
use crate::theme::UiTheme;
#[derive(Debug, Clone, Copy, Default)]
pub struct Viewport {
pub top_line: usize,
pub left_col: usize,
pub height: usize,
pub width: usize,
}
impl Viewport {
pub fn ensure_cursor_visible(
&mut self,
cursor: Position,
total_lines: usize,
viewport_height: usize,
text_width: usize,
) {
self.height = viewport_height;
self.width = text_width;
let margin = 2usize.min(viewport_height.saturating_div(3));
if cursor.line < self.top_line + margin {
self.top_line = cursor.line.saturating_sub(margin);
} else if cursor.line >= self.top_line + viewport_height.saturating_sub(margin) {
self.top_line = cursor
.line
.saturating_sub(viewport_height.saturating_sub(margin + 1));
}
let max_top = total_lines.saturating_sub(viewport_height.max(1));
self.top_line = self.top_line.min(max_top);
if cursor.column < self.left_col {
self.left_col = cursor.column;
} else if cursor.column >= self.left_col + text_width.max(1) {
self.left_col = cursor.column.saturating_sub(text_width.saturating_sub(1));
}
}
}
pub fn gutter_width(total_lines: usize, show_line_numbers: bool) -> u16 {
if show_line_numbers {
total_lines.max(1).to_string().len() as u16 + 3
} else {
0
}
}
pub fn render_editor(
frame: &mut Frame<'_>,
area: Rect,
editor: &Editor,
viewport: &Viewport,
config: &Config,
theme: &UiTheme,
highlights: &[HighlightSpan],
) {
let total_lines = editor.line_count();
let gutter = gutter_width(total_lines, config.show_line_numbers);
let text_width = area.width.saturating_sub(gutter) as usize;
let mut lines = Vec::with_capacity(area.height as usize);
let cursor_line = editor.cursor().position.line;
for screen_row in 0..area.height as usize {
let line_idx = viewport.top_line + screen_row;
if line_idx < total_lines {
let mut spans = Vec::new();
if config.show_line_numbers {
let number = format!(
"{:>width$} │ ",
line_idx + 1,
width = (gutter as usize).saturating_sub(3)
);
let style = if line_idx == cursor_line {
Style::default()
.fg(theme.gutter_active)
.bg(theme.background)
} else {
Style::default().fg(theme.gutter).bg(theme.background)
};
spans.push(Span::styled(number, style));
}
let text = editor.line_text_without_ending(line_idx);
spans.extend(styled_line_spans(
line_idx,
&text,
viewport.left_col,
text_width,
highlights,
theme,
));
lines.push(Line::from(spans));
} else {
let blank = if config.show_line_numbers {
" ".repeat(gutter as usize)
} else {
String::new()
};
lines.push(Line::from(Span::styled(blank, theme.normal())));
}
}
frame.render_widget(Paragraph::new(lines).style(theme.normal()), area);
let cursor = editor.cursor().position;
if cursor.line >= viewport.top_line
&& cursor.line < viewport.top_line + area.height as usize
&& cursor.column >= viewport.left_col
&& cursor.column < viewport.left_col + text_width.max(1)
{
let x = area.x + gutter + (cursor.column - viewport.left_col) as u16;
let y = area.y + (cursor.line - viewport.top_line) as u16;
if x < area.x + area.width && y < area.y + area.height {
frame.set_cursor_position((x, y));
}
}
}
fn styled_line_spans(
line_idx: usize,
text: &str,
left_col: usize,
width: usize,
highlights: &[HighlightSpan],
theme: &UiTheme,
) -> Vec<Span<'static>> {
let chars: Vec<char> = text.chars().collect();
if width == 0 {
return Vec::new();
}
let mut kinds = vec![HighlightKind::Normal; chars.len()];
for span in highlights
.iter()
.filter(|span| span.intersects_line(line_idx))
{
let start = if line_idx == span.line_start {
span.column_start
} else {
0
};
let end = if line_idx == span.line_end {
span.column_end
} else {
chars.len()
};
let start = start.min(chars.len());
let end = end.min(chars.len());
for kind in kinds.iter_mut().take(end).skip(start) {
*kind = span.kind;
}
}
let start = left_col.min(chars.len());
let end = (start + width).min(chars.len());
if start >= end {
return vec![Span::raw("")];
}
let mut spans = Vec::new();
let mut current_kind = kinds[start];
let mut current = String::new();
for idx in start..end {
let kind = kinds[idx];
if kind != current_kind && !current.is_empty() {
spans.push(Span::styled(
current.clone(),
theme.syntax_style(current_kind),
));
current.clear();
current_kind = kind;
}
current.push(chars[idx]);
}
if !current.is_empty() {
spans.push(Span::styled(current, theme.syntax_style(current_kind)));
}
spans
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_tracks_cursor() {
let mut viewport = Viewport::default();
viewport.ensure_cursor_visible(Position::new(50, 10), 100, 10, 20);
assert!(viewport.top_line <= 50);
assert!(viewport.top_line + 10 > 50);
}
}