use std::{borrow::Cow, cell::RefCell};
use {
super::*,
crate::{
BufferId, CursorInfo, LineNumberMode, SyntaxToken, ViewportContext, ViewportRenderer,
VirtualLine, VirtualLinePosition,
},
};
struct RecordingSurface {
width: u16,
height: u16,
writes: RefCell<Vec<(u16, u16, String, Style)>>,
overlays: RefCell<Vec<(u16, u16, reovim_arch::Color)>>,
}
impl RecordingSurface {
fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
writes: RefCell::new(Vec::new()),
overlays: RefCell::new(Vec::new()),
}
}
fn text_at(&self, y: u16) -> String {
let writes = self.writes.borrow();
let mut chars: Vec<(u16, String)> = writes
.iter()
.filter(|(_, wy, _, _)| *wy == y)
.map(|(wx, _, text, _)| (*wx, text.clone()))
.collect();
chars.sort_by_key(|(x, _)| *x);
chars.into_iter().map(|(_, t)| t).collect()
}
}
impl RenderSurface for RecordingSurface {
#[allow(clippy::cast_possible_truncation)]
fn write_styled(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
let width = text.chars().count() as u16;
self.writes
.borrow_mut()
.push((x, y, text.to_owned(), style));
width
}
fn apply_style(&mut self, _x: u16, _y: u16, _style: Style) {}
fn overlay_bg(&mut self, x: u16, y: u16, bg: reovim_arch::Color) {
self.overlays.borrow_mut().push((x, y, bg));
}
fn fill(&mut self, _rect: Rect, _ch: char, _style: Style) {}
fn clear(&mut self, _rect: Rect) {}
fn size(&self) -> (u16, u16) {
(self.width, self.height)
}
}
struct MockTokenProvider {
tokens: Vec<SyntaxToken>,
}
impl MockTokenProvider {
fn empty() -> Self {
Self { tokens: Vec::new() }
}
fn with_tokens(tokens: Vec<SyntaxToken>) -> Self {
Self { tokens }
}
}
impl TokenProvider for MockTokenProvider {
fn tokens_for_line(&self, _buffer_id: BufferId, _line: u32) -> Vec<SyntaxToken> {
self.tokens.clone()
}
}
struct MockTheme;
impl ThemeProvider for MockTheme {
fn highlight(&self, group: &str) -> Style {
match group {
"keyword" => Style::new().fg(reovim_arch::Color::Blue),
"string" => Style::new().fg(reovim_arch::Color::Green),
"comment" => Style::new().fg(reovim_arch::Color::DarkGreen),
"bg.token" => Style::new().bg(reovim_arch::Color::Red),
_ => Style::default(),
}
}
fn highlight_with_fallback(&self, groups: &[&str]) -> Style {
groups
.first()
.map_or_else(Style::default, |g| self.highlight(g))
}
fn foreground(&self) -> Style {
Style::new().fg(reovim_arch::Color::White)
}
fn background(&self) -> Style {
Style::new().bg(reovim_arch::Color::Black)
}
fn is_dark(&self) -> bool {
true
}
}
fn empty_ctx<'a>() -> ViewportContext<'a> {
ViewportContext {
buffer_id: None,
buffer_lines: None,
cursor: None,
scroll_top: 0,
local_selection: None,
remote_clients: &[],
fold_ranges: &[],
virtual_lines: &[],
opacity: 1.0,
line_number_mode: LineNumberMode::None,
gutter_width: 0,
sidebar_width: 0,
is_insert_mode: false,
render_self_cursor: false,
my_client_id: 0,
}
}
#[test]
fn is_line_folded_empty_ranges() {
assert!(!is_line_folded(0, &[]));
}
#[test]
fn is_line_folded_in_range() {
let folds = [(5, 3)]; assert!(!is_line_folded(4, &folds));
assert!(is_line_folded(5, &folds));
assert!(is_line_folded(6, &folds));
assert!(is_line_folded(7, &folds));
assert!(!is_line_folded(8, &folds));
}
#[test]
fn is_line_folded_multiple_ranges() {
let folds = [(2, 2), (10, 1)]; assert!(is_line_folded(2, &folds));
assert!(is_line_folded(3, &folds));
assert!(!is_line_folded(4, &folds));
assert!(is_line_folded(10, &folds));
assert!(!is_line_folded(11, &folds));
}
#[test]
fn buffer_to_screen_row_no_virtual_lines() {
assert_eq!(buffer_to_screen_row_vl(5, 0, &[]), 5);
assert_eq!(buffer_to_screen_row_vl(10, 5, &[]), 5);
}
#[test]
fn buffer_to_screen_row_with_virtual_lines() {
let vl = vec![VirtualLine {
buffer_line: 3,
position: VirtualLinePosition::Before,
content: "diag".to_string(),
style: Style::default(),
}];
assert_eq!(buffer_to_screen_row_vl(2, 0, &vl), 2);
assert_eq!(buffer_to_screen_row_vl(3, 0, &vl), 4); assert_eq!(buffer_to_screen_row_vl(5, 0, &vl), 6); }
#[test]
fn buffer_to_screen_row_vl_before_scroll() {
let vl = vec![VirtualLine {
buffer_line: 1,
position: VirtualLinePosition::After,
content: "x".to_string(),
style: Style::default(),
}];
assert_eq!(buffer_to_screen_row_vl(5, 5, &vl), 0);
}
#[test]
fn apply_opacity_full() {
let style = Style::new().fg(reovim_arch::Color::Red);
let result = apply_opacity(&style, 1.0);
assert_eq!(result, style);
}
#[test]
fn apply_opacity_partial() {
let style = Style::new().fg(reovim_arch::Color::White);
let result = apply_opacity(&style, 0.5);
assert!(result.fg.is_some());
assert_ne!(result, style);
}
#[test]
fn classify_with_modules_no_modules() {
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
assert_eq!(classify_with_modules(&modules, "keyword"), RenderBehavior::Highlight);
}
#[test]
fn classify_with_modules_no_buffer_contrib() {
struct NonContribModule;
impl ClientModule for NonContribModule {
fn id(&self) -> &'static str {
"non-contrib"
}
fn name(&self) -> &'static str {
"Non-Contrib"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
false
}
}
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(NonContribModule)];
assert_eq!(classify_with_modules(&modules, "keyword"), RenderBehavior::Highlight);
}
#[test]
fn transform_line_no_modules() {
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
assert!(transform_line(&modules, Some(BufferId(0)), 0, "hello").is_none());
}
#[test]
fn transform_line_no_buffer_id() {
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
assert!(transform_line(&modules, None, 0, "hello").is_none());
}
#[test]
fn render_virtual_line_basic() {
let mut surface = RecordingSurface::new(80, 24);
let style = Style::new().fg(reovim_arch::Color::Grey);
render_virtual_line(&mut surface, 4, 0, 20, "---table---", &style);
let text = surface.text_at(0);
assert_eq!(text, "---table---");
}
#[test]
fn render_virtual_line_truncated() {
let mut surface = RecordingSurface::new(80, 24);
render_virtual_line(&mut surface, 0, 0, 5, "hello world", &Style::default());
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_transformed_line_single_segment() {
let mut surface = RecordingSurface::new(80, 24);
let transformed = TransformedLine {
segments: vec![("hello".to_string(), None)],
};
render_transformed_line(&mut surface, 4, 0, 20, &transformed);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_transformed_line_multi_segment() {
let mut surface = RecordingSurface::new(80, 24);
let style = Style::new().fg(reovim_arch::Color::Red);
let transformed = TransformedLine {
segments: vec![("fn ".to_string(), Some(style)), ("main".to_string(), None)],
};
render_transformed_line(&mut surface, 0, 0, 20, &transformed);
let text = surface.text_at(0);
assert_eq!(text, "fn main");
}
#[test]
fn render_transformed_line_truncated() {
let mut surface = RecordingSurface::new(80, 24);
let transformed = TransformedLine {
segments: vec![("hello world foo bar".to_string(), None)],
};
render_transformed_line(&mut surface, 0, 0, 5, &transformed);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_line_content_plain_text() {
let mut surface = RecordingSurface::new(80, 24);
let token_provider = MockTokenProvider::empty();
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
80,
"hello",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_line_content_with_highlight_tokens() {
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 2,
category: "keyword".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
80,
"fn main",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "fn main");
let writes = surface.writes.borrow();
assert_eq!(
writes[0].3.fg,
Some(reovim_arch::Color::Blue),
"first char should be keyword-styled"
);
}
#[test]
fn render_line_content_narrowest_token_wins() {
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![
SyntaxToken {
line: 0,
start_col: 0,
end_col: 15, category: "comment".to_owned(),
},
SyntaxToken {
line: 0,
start_col: 4,
end_col: 6, category: "keyword".to_owned(),
},
];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
80,
"/// fn main() {}",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let writes = surface.writes.borrow();
assert_eq!(
writes[0].3.fg,
Some(reovim_arch::Color::DarkGreen),
"col 0 should be comment-styled (DarkGreen)"
);
assert_eq!(
writes[4].3.fg,
Some(reovim_arch::Color::Blue),
"col 4 should be keyword-styled (Blue), not comment"
);
assert_eq!(
writes[6].3.fg,
Some(reovim_arch::Color::DarkGreen),
"col 6 should be comment-styled"
);
}
#[test]
fn render_line_content_no_buffer_id() {
let mut surface = RecordingSurface::new(80, 24);
let token_provider = MockTokenProvider::empty();
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
80,
"hello",
1.0,
None, 0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_line_content_with_opacity() {
let mut surface = RecordingSurface::new(80, 24);
let token_provider = MockTokenProvider::empty();
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
80,
"hi",
0.5, Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hi");
}
#[test]
fn render_line_content_truncated() {
let mut surface = RecordingSurface::new(80, 24);
let token_provider = MockTokenProvider::empty();
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_line_content(
&mut surface,
0,
0,
3, "hello world",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hel");
}
#[test]
fn render_line_content_with_conceal() {
struct ConcealModule;
impl ClientModule for ConcealModule {
fn id(&self) -> &'static str {
"conceal"
}
fn name(&self) -> &'static str {
"Conceal"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "conceal.url" {
Some(RenderBehavior::Conceal {
replacement: Cow::Borrowed("..."),
})
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 5,
end_col: 25,
category: "conceal.url".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ConcealModule)];
render_line_content(
&mut surface,
0,
0,
80,
"text http://example.com rest",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert!(text.contains("..."), "URL should be concealed: {text}");
assert!(!text.contains("http"), "URL should not be visible: {text}");
}
#[test]
fn render_line_content_conceal_skip_in_insert() {
struct ConcealModule;
impl ClientModule for ConcealModule {
fn id(&self) -> &'static str {
"conceal"
}
fn name(&self) -> &'static str {
"Conceal"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "conceal.url" {
Some(RenderBehavior::Hide)
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 5,
end_col: 10,
category: "conceal.url".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ConcealModule)];
render_line_content(
&mut surface,
0,
0,
80,
"hello world",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
true, &modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello world", "conceals should be skipped in insert mode");
}
#[test]
fn render_line_content_with_background() {
struct BgModule;
impl ClientModule for BgModule {
fn id(&self) -> &'static str {
"bg"
}
fn name(&self) -> &'static str {
"Bg"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "bg.token" {
Some(RenderBehavior::Background(reovim_arch::Color::Red))
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 5,
category: "bg.token".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(BgModule)];
render_line_content(
&mut surface,
0,
0,
80,
"hello",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let overlays = surface.overlays.borrow();
assert!(!overlays.is_empty(), "should have background overlays");
}
#[test]
fn render_line_content_with_hide() {
struct HideModule;
impl ClientModule for HideModule {
fn id(&self) -> &'static str {
"hide"
}
fn name(&self) -> &'static str {
"Hide"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "hidden" {
Some(RenderBehavior::Hide)
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 5,
end_col: 11,
category: "hidden".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(HideModule)];
render_line_content(
&mut surface,
0,
0,
80,
"hello world",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello", "hidden text should not be visible");
}
#[test]
fn render_line_content_full_width_line() {
struct FwlModule;
impl ClientModule for FwlModule {
fn id(&self) -> &'static str {
"fwl"
}
fn name(&self) -> &'static str {
"FWL"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "hr" {
Some(RenderBehavior::FullWidthLine {
ch: '-',
style: Style::default(),
})
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 3,
category: "hr".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(FwlModule)];
render_line_content(
&mut surface,
0,
0,
10,
"---",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert!(text.contains("----------"), "should be full-width dashes: {text}");
}
#[test]
fn render_buffer_content_basic() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "hello");
assert_eq!(surface.text_at(1), "world");
}
#[test]
fn render_buffer_content_with_scroll() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec![
"line0".to_string(),
"line1".to_string(),
"line2".to_string(),
];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
scroll_top: 1,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "line1");
assert_eq!(surface.text_at(1), "line2");
}
#[test]
fn render_buffer_content_past_end_shows_tilde() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 3);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "hello");
assert_eq!(surface.text_at(1), "~");
}
#[test]
fn render_buffer_content_with_folds() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec![
"line0".to_string(),
"line1".to_string(),
"line2".to_string(),
"line3".to_string(),
];
let folds = vec![(1, 2)]; let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
fold_ranges: &folds,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "line0");
assert_eq!(surface.text_at(1), "line3");
}
#[test]
fn render_buffer_content_with_virtual_lines_before() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let vlines = vec![VirtualLine {
buffer_line: 1,
position: VirtualLinePosition::Before,
content: "-- virtual --".to_string(),
style: Style::default(),
}];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
virtual_lines: &vlines,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "hello");
assert_eq!(surface.text_at(1), "-- virtual --");
assert_eq!(surface.text_at(2), "world");
}
#[test]
fn render_buffer_content_with_virtual_lines_after() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let vlines = vec![VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::After,
content: "-- after --".to_string(),
style: Style::default(),
}];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
virtual_lines: &vlines,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "hello");
assert_eq!(surface.text_at(1), "-- after --");
assert_eq!(surface.text_at(2), "world");
}
#[test]
fn render_buffer_content_with_gutter_annotations() {
struct MockAnnotation;
impl ClientModule for MockAnnotation {
fn id(&self) -> &'static str {
"mock-ann"
}
fn name(&self) -> &'static str {
"Mock"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_annotations(&self) -> bool {
true
}
fn annotation_priority(&self) -> u16 {
100
}
fn annotation_column_width(
&self,
_ctx: &crate::AnnotationContext,
_caps: &dyn crate::PlatformCapabilities,
) -> crate::ColumnWidth {
crate::ColumnWidth::Fixed(4)
}
fn annotate(
&self,
line: usize,
_ctx: &crate::AnnotationContext,
) -> Option<crate::GutterCell> {
Some(crate::GutterCell {
text: (line + 1).to_string(),
style: Style::new().fg(reovim_arch::Color::DarkGrey),
})
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo { line: 0, column: 0 }),
gutter_width: 4,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(MockAnnotation)];
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 84, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
let row0 = surface.text_at(0);
assert!(row0.contains('1'), "should have line number 1: {row0}");
assert!(row0.contains("hello"), "should have content: {row0}");
}
#[test]
fn render_buffer_content_no_buffer_lines() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: None, ..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 3);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert!(surface.writes.borrow().is_empty());
}
#[test]
fn default_viewport_renderer_gutter_width_no_annotations() {
let renderer = DefaultViewportRenderer;
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let caps = TestCaps;
assert_eq!(renderer.gutter_width(&modules, &caps), 0);
}
#[test]
fn default_viewport_renderer_render_viewport() {
let renderer = DefaultViewportRenderer;
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
renderer.render_viewport(
&mut surface,
viewport,
&ctx,
&modules,
&token_provider,
&MockTheme,
&TestCaps,
);
assert_eq!(surface.text_at(0), "hello");
}
struct TestCaps;
impl PlatformCapabilities for TestCaps {
fn rendering_model(&self) -> crate::RenderingModel {
crate::RenderingModel::CellGrid
}
fn grid_size(&self) -> Option<(u16, u16)> {
Some((80, 24))
}
fn color_depth(&self) -> crate::ColorDepth {
crate::ColorDepth::TrueColor
}
fn pixel_size(&self) -> Option<(u32, u32)> {
None
}
fn reliable_unicode_width(&self) -> bool {
true
}
fn dark_mode(&self) -> bool {
true
}
fn smooth_scroll(&self) -> bool {
false
}
fn pointer_events(&self) -> bool {
false
}
fn touch_input(&self) -> bool {
false
}
fn haptic(&self) -> bool {
false
}
fn safe_area(&self) -> crate::Insets {
crate::Insets::ZERO
}
fn has_focus(&self) -> bool {
true
}
fn clipboard_available(&self) -> bool {
true
}
fn screen_reader_active(&self) -> bool {
false
}
}
#[test]
fn normalize_selection_already_normalized() {
let sel = SelectionInfo {
start_line: 1,
start_col: 0,
end_line: 3,
end_col: 5,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let (sl, sc, el, ec) = normalize_selection(&sel);
assert_eq!((sl, sc, el, ec), (1, 0, 3, 5));
}
#[test]
fn normalize_selection_reversed() {
let sel = SelectionInfo {
start_line: 5,
start_col: 10,
end_line: 2,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let (sl, sc, el, ec) = normalize_selection(&sel);
assert_eq!((sl, sc, el, ec), (2, 3, 5, 10));
}
#[test]
fn render_selections_no_selections() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = empty_ctx();
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
assert!(surface.overlays.borrow().is_empty());
}
#[test]
fn render_selections_local_char_selection() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 2,
end_line: 0,
end_col: 5,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 4);
}
#[test]
fn render_selections_line_mode() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
mode: crate::SelectionMode::Line,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 80);
}
#[test]
fn render_selections_remote_client() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello world".to_string()];
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 0,
mode: "visual".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: Some(SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::DarkBlue,
}),
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
remote_clients: &remotes,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 4); }
#[test]
fn render_remote_cursors_empty() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = empty_ctx();
render_remote_cursors(&mut surface, &ctx, 0, 24);
}
#[test]
fn render_remote_cursors_visible() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 2,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursors(&mut surface, &ctx, 0, 24);
}
#[test]
fn render_remote_cursors_before_scroll() {
let mut surface = RecordingSurface::new(80, 24);
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Bob".to_string(),
cursor_line: 2,
cursor_col: 0,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Green,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
scroll_top: 5,
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursors(&mut surface, &ctx, 0, 24);
}
#[test]
fn render_self_cursor_no_cursor() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = ViewportContext {
cursor: None,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn render_self_cursor_visible() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo { line: 0, column: 2 }),
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn render_self_cursor_insert_mode() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo { line: 0, column: 3 }),
is_insert_mode: true,
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn render_self_cursor_before_scroll() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = ViewportContext {
cursor: Some(CursorInfo { line: 0, column: 0 }),
scroll_top: 5,
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn compute_cursor_visual_col_no_buffer() {
let ctx = ViewportContext {
buffer_id: None,
..empty_ctx()
};
let cursor = CursorInfo { line: 0, column: 5 };
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
assert_eq!(compute_cursor_visual_col(&ctx, &cursor, &tokens, &MockTheme, &modules), 5);
}
#[test]
fn compute_cursor_visual_col_no_content() {
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: None,
..empty_ctx()
};
let cursor = CursorInfo { line: 0, column: 3 };
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
assert_eq!(compute_cursor_visual_col(&ctx, &cursor, &tokens, &MockTheme, &modules), 3);
}
#[test]
fn compute_cursor_visual_col_no_conceals() {
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let cursor = CursorInfo { line: 0, column: 3 };
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
assert_eq!(compute_cursor_visual_col(&ctx, &cursor, &tokens, &MockTheme, &modules), 3);
}
#[test]
fn label_text_normal() {
let label = label_text("Alice", "normal");
assert!(label.contains("Alice"));
assert!(label.contains("[N]"));
}
#[test]
fn label_text_insert() {
let label = label_text("Bob", "insert");
assert!(label.contains("Bob"));
assert!(label.contains("[I]"));
}
#[test]
fn label_text_visual() {
let label = label_text("Carol", "visual");
assert!(label.contains("[V]"));
}
#[test]
fn label_text_command() {
let label = label_text("Dan", "command");
assert!(label.contains("[C]"));
}
#[test]
fn label_text_replace() {
let label = label_text("Eve", "replace");
assert!(label.contains("[R]"));
}
#[test]
fn label_text_empty_name() {
let label = label_text("", "normal");
assert!(label.contains('?'));
}
#[test]
fn mode_abbreviation_variants() {
assert_eq!(mode_abbreviation("normal"), "[N]");
assert_eq!(mode_abbreviation("INSERT"), "[I]");
assert_eq!(mode_abbreviation("visual line"), "[V]");
assert_eq!(mode_abbreviation("cmdline"), "[C]");
assert_eq!(mode_abbreviation("REPLACE"), "[R]");
assert_eq!(mode_abbreviation("unknown"), "[N]");
}
#[test]
fn client_color_palette() {
let c0 = client_color(0);
let c1 = client_color(1);
assert_ne!(format!("{c0:?}"), format!("{c1:?}"));
let c8 = client_color(8);
assert_eq!(format!("{c0:?}"), format!("{c8:?}"));
}
#[test]
fn render_viewport_with_local_selection() {
let renderer = DefaultViewportRenderer;
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 4,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
renderer.render_viewport(
&mut surface,
viewport,
&ctx,
&modules,
&token_provider,
&MockTheme,
&TestCaps,
);
assert_eq!(surface.text_at(0), "hello world");
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 5, "should have 5 overlay columns (0..=4)");
}
#[test]
fn render_viewport_with_self_cursor() {
let renderer = DefaultViewportRenderer;
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo { line: 0, column: 2 }),
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
renderer.render_viewport(
&mut surface,
viewport,
&ctx,
&modules,
&token_provider,
&MockTheme,
&TestCaps,
);
assert_eq!(surface.text_at(0), "hello");
}
#[test]
fn render_selection_char_two_lines() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 2,
end_line: 1,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 3 + 4, "2-line Char selection overlay count");
}
#[test]
fn render_selection_char_three_lines() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["aaaa".to_string(), "bbbb".to_string(), "cccc".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 1,
end_line: 2,
end_col: 2,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Green,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 3 + 4 + 3, "3-line Char selection overlay count");
}
#[test]
fn render_selection_block_mode() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["aaaa".to_string(), "bbbb".to_string(), "cccc".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 1,
end_line: 2,
end_col: 2,
mode: crate::SelectionMode::Block,
color: reovim_arch::Color::Red,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 6, "Block selection overlay count");
}
#[test]
fn render_gutter_annotations_col_width_zero() {
struct ZeroWidthAnnotation;
impl ClientModule for ZeroWidthAnnotation {
fn id(&self) -> &'static str {
"zero-ann"
}
fn name(&self) -> &'static str {
"Zero"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_annotations(&self) -> bool {
true
}
fn annotation_column_width(
&self,
_ctx: &crate::AnnotationContext,
_caps: &dyn crate::PlatformCapabilities,
) -> crate::ColumnWidth {
crate::ColumnWidth::Fixed(0)
}
fn annotate(
&self,
_line: usize,
_ctx: &crate::AnnotationContext,
) -> Option<crate::GutterCell> {
Some(crate::GutterCell {
text: "X".to_string(),
style: Style::default(),
})
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
gutter_width: 4,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ZeroWidthAnnotation)];
render_gutter_annotations(&mut surface, 0, 0, 4, 0, &modules, &ctx);
assert!(
surface.writes.borrow().is_empty(),
"zero-width annotation should produce no output"
);
}
#[test]
fn render_gutter_annotations_annotate_returns_none() {
struct NoneAnnotation;
impl ClientModule for NoneAnnotation {
fn id(&self) -> &'static str {
"none-ann"
}
fn name(&self) -> &'static str {
"None"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_annotations(&self) -> bool {
true
}
fn annotation_column_width(
&self,
_ctx: &crate::AnnotationContext,
_caps: &dyn crate::PlatformCapabilities,
) -> crate::ColumnWidth {
crate::ColumnWidth::Fixed(4)
}
fn annotate(
&self,
_line: usize,
_ctx: &crate::AnnotationContext,
) -> Option<crate::GutterCell> {
None
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
gutter_width: 4,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(NoneAnnotation)];
render_gutter_annotations(&mut surface, 0, 0, 4, 0, &modules, &ctx);
assert!(surface.writes.borrow().is_empty(), "None annotate should produce no output");
}
#[test]
fn render_gutter_annotations_dynamic_width() {
struct DynamicAnnotation;
impl ClientModule for DynamicAnnotation {
fn id(&self) -> &'static str {
"dyn-ann"
}
fn name(&self) -> &'static str {
"Dyn"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_annotations(&self) -> bool {
true
}
fn annotation_column_width(
&self,
_ctx: &crate::AnnotationContext,
_caps: &dyn crate::PlatformCapabilities,
) -> crate::ColumnWidth {
crate::ColumnWidth::Dynamic(3)
}
fn annotate(
&self,
line: usize,
_ctx: &crate::AnnotationContext,
) -> Option<crate::GutterCell> {
Some(crate::GutterCell {
text: (line + 1).to_string(),
style: Style::default(),
})
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
gutter_width: 3,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(DynamicAnnotation)];
render_gutter_annotations(&mut surface, 0, 0, 3, 0, &modules, &ctx);
let writes = surface.writes.borrow();
assert!(!writes.is_empty(), "Dynamic annotation should produce output");
let text: String = writes.iter().map(|(_, _, t, _)| t.as_str()).collect();
assert!(text.contains('1'), "should contain line number 1: {text}");
}
#[test]
fn compute_cursor_visual_col_with_conceals() {
struct ConcealModule;
impl ClientModule for ConcealModule {
fn id(&self) -> &'static str {
"conceal"
}
fn name(&self) -> &'static str {
"Conceal"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "conceal.url" {
Some(RenderBehavior::Conceal {
replacement: Cow::Borrowed("L"),
})
} else {
None
}
}
}
let lines = vec!["abhttp://ex.comcd".to_string()];
let tokens = vec![SyntaxToken {
line: 0,
start_col: 2,
end_col: 15,
category: "conceal.url".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ConcealModule)];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let cursor = CursorInfo {
line: 0,
column: 16,
};
let result = compute_cursor_visual_col(&ctx, &cursor, &token_provider, &MockTheme, &modules);
assert_eq!(result, 4, "cursor should be at display col 4 after conceal");
}
#[test]
fn compute_cursor_visual_col_with_hide() {
struct HideModule;
impl ClientModule for HideModule {
fn id(&self) -> &'static str {
"hide"
}
fn name(&self) -> &'static str {
"Hide"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "hidden" {
Some(RenderBehavior::Hide)
} else {
None
}
}
}
let lines = vec!["hello world".to_string()];
let tokens = vec![SyntaxToken {
line: 0,
start_col: 5,
end_col: 11,
category: "hidden".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(HideModule)];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let cursor = CursorInfo { line: 0, column: 3 };
let result = compute_cursor_visual_col(&ctx, &cursor, &token_provider, &MockTheme, &modules);
assert_eq!(result, 3, "cursor before hidden range should be unchanged");
}
#[test]
fn compute_cursor_visual_col_with_full_width_line() {
struct FwlModule;
impl ClientModule for FwlModule {
fn id(&self) -> &'static str {
"fwl"
}
fn name(&self) -> &'static str {
"FWL"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "hr" {
Some(RenderBehavior::FullWidthLine {
ch: '-',
style: Style::default(),
})
} else {
None
}
}
}
let lines = vec!["---".to_string()];
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 3,
category: "hr".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(FwlModule)];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let cursor = CursorInfo { line: 0, column: 0 };
let result = compute_cursor_visual_col(&ctx, &cursor, &token_provider, &MockTheme, &modules);
assert!(result < 600, "cursor visual col should be reasonable");
}
#[test]
fn render_remote_cursor_labels_visible() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 2,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursor_labels(&mut surface, &ctx, 0, 24);
let writes = surface.writes.borrow();
assert!(!writes.is_empty(), "should render a cursor label");
let text: String = writes.iter().map(|(_, _, t, _)| t.as_str()).collect();
assert!(text.contains("Alice"), "label should contain display name: {text}");
assert!(text.contains("[N]"), "label should contain mode abbreviation: {text}");
}
#[test]
fn render_remote_cursor_labels_overflow_skip() {
let mut surface = RecordingSurface::new(10, 24);
let lines = vec!["hello".to_string()];
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "VeryLongName".to_string(),
cursor_line: 0,
cursor_col: 2,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursor_labels(&mut surface, &ctx, 0, 24);
assert!(surface.writes.borrow().is_empty(), "label should be skipped when it overflows");
}
#[test]
fn render_remote_cursor_labels_before_scroll() {
let mut surface = RecordingSurface::new(80, 24);
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 0,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
scroll_top: 5,
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursor_labels(&mut surface, &ctx, 0, 24);
assert!(
surface.writes.borrow().is_empty(),
"label should be skipped when cursor is before scroll"
);
}
#[test]
fn render_remote_cursor_labels_past_content_height() {
let mut surface = RecordingSurface::new(80, 24);
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Bob".to_string(),
cursor_line: 30,
cursor_col: 0,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursor_labels(&mut surface, &ctx, 0, 5);
assert!(
surface.writes.borrow().is_empty(),
"label should be skipped when cursor is past content_height"
);
}
#[test]
fn render_buffer_content_vl_before_fills_viewport() {
let mut surface = RecordingSurface::new(80, 2);
let lines = vec!["hello".to_string(), "world".to_string()];
let vlines = vec![
VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::Before,
content: "vl1".to_string(),
style: Style::default(),
},
VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::Before,
content: "vl2".to_string(),
style: Style::default(),
},
];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
virtual_lines: &vlines,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 2);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
assert_eq!(surface.text_at(0), "vl1");
assert_eq!(surface.text_at(1), "vl2");
}
#[test]
fn render_line_content_with_inline_decorations() {
struct InlineDecModule {
decorations: Vec<crate::InlineDecoration>,
}
impl ClientModule for InlineDecModule {
fn id(&self) -> &'static str {
"inline-dec"
}
fn name(&self) -> &'static str {
"InlineDec"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn inline_decorations(&self, _line: usize) -> &[crate::InlineDecoration] {
&self.decorations
}
}
let mut surface = RecordingSurface::new(80, 24);
let token_provider = MockTokenProvider::empty();
let module = InlineDecModule {
decorations: vec![crate::InlineDecoration {
col_start: 0,
col_end: 3,
style: Style::new().fg(reovim_arch::Color::Red),
}],
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(module)];
render_line_content(
&mut surface,
0,
0,
80,
"hello",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn default_viewport_renderer_gutter_width_dynamic() {
struct DynAnnotation;
impl ClientModule for DynAnnotation {
fn id(&self) -> &'static str {
"dyn-gutter"
}
fn name(&self) -> &'static str {
"DynGutter"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_annotations(&self) -> bool {
true
}
fn annotation_column_width(
&self,
_ctx: &crate::AnnotationContext,
_caps: &dyn crate::PlatformCapabilities,
) -> crate::ColumnWidth {
crate::ColumnWidth::Dynamic(5)
}
}
let renderer = DefaultViewportRenderer;
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(DynAnnotation)];
let caps = TestCaps;
assert_eq!(renderer.gutter_width(&modules, &caps), 5, "Dynamic(5) should use min=5");
}
#[test]
fn dimmed_client_color_palette() {
let c0 = dimmed_client_color(0);
let c1 = dimmed_client_color(1);
assert_ne!(format!("{c0:?}"), format!("{c1:?}"));
let c8 = dimmed_client_color(8);
assert_eq!(format!("{c0:?}"), format!("{c8:?}"));
}
#[test]
fn render_selection_before_scroll() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec![
"line0".to_string(),
"line1".to_string(),
"line2".to_string(),
];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
scroll_top: 2,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
assert!(
surface.overlays.borrow().is_empty(),
"selection before scroll should produce no overlays"
);
}
#[test]
fn render_selection_past_content_height() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["line0".to_string()];
let local_sel = SelectionInfo {
start_line: 10,
start_col: 0,
end_line: 10,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 3, &modules);
assert!(
surface.overlays.borrow().is_empty(),
"selection past content_height should produce no overlays"
);
}
#[test]
fn render_self_cursor_with_column_mapping() {
struct ColMapModule;
impl ClientModule for ColMapModule {
fn id(&self) -> &'static str {
"colmap"
}
fn name(&self) -> &'static str {
"ColMap"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
#[allow(clippy::cast_possible_truncation)]
fn map_cursor_column(&self, _buf: BufferId, _line: usize, col: usize) -> Option<u16> {
Some((col + 5) as u16)
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo { line: 0, column: 2 }),
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ColMapModule)];
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn render_remote_cursor_labels_no_buffer_lines() {
let mut surface = RecordingSurface::new(80, 24);
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 0,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_lines: None,
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursor_labels(&mut surface, &ctx, 0, 24);
let writes = surface.writes.borrow();
assert!(!writes.is_empty(), "should still render label at col 1");
}
#[test]
fn render_selection_char_with_column_mapping_module() {
struct ColMapModule;
impl ClientModule for ColMapModule {
fn id(&self) -> &'static str {
"colmap"
}
fn name(&self) -> &'static str {
"ColMap"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
#[allow(clippy::cast_possible_truncation)]
fn map_cursor_column(&self, _buf: BufferId, _line: usize, col: usize) -> Option<u16> {
Some((col + 2) as u16)
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 1,
end_line: 0,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ColMapModule)];
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 3, "column-mapped Char selection overlay count");
}
#[test]
fn render_selection_block_with_column_mapping_module() {
struct ColMapModule;
impl ClientModule for ColMapModule {
fn id(&self) -> &'static str {
"colmap"
}
fn name(&self) -> &'static str {
"ColMap"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
#[allow(clippy::cast_possible_truncation)]
fn map_cursor_column(&self, _buf: BufferId, _line: usize, col: usize) -> Option<u16> {
Some((col + 2) as u16)
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 1,
end_line: 1,
end_col: 2,
mode: crate::SelectionMode::Block,
color: reovim_arch::Color::Red,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ColMapModule)];
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 4, "block selection with column mapping overlay count");
}
#[test]
fn render_selection_with_transform_line_visual_width() {
struct TransformModule;
impl ClientModule for TransformModule {
fn id(&self) -> &'static str {
"transform"
}
fn name(&self) -> &'static str {
"Transform"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn transform_line(
&self,
_buf: BufferId,
_line: usize,
_text: &str,
) -> Option<crate::TransformedLine> {
Some(crate::TransformedLine {
segments: vec![("ab".to_string(), None), ("cd".to_string(), None)],
})
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 2,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(TransformModule)];
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 3, "selection with transform_line visual width");
}
#[test]
fn render_selection_no_buffer_lines() {
let mut surface = RecordingSurface::new(80, 24);
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: None,
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 4, "selection with no buffer_lines defaults to content_width");
}
#[test]
fn render_buffer_content_with_transformed_line() {
struct TransformModule;
impl ClientModule for TransformModule {
fn id(&self) -> &'static str {
"transform"
}
fn name(&self) -> &'static str {
"Transform"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn transform_line(
&self,
_buf: BufferId,
_line: usize,
_text: &str,
) -> Option<crate::TransformedLine> {
Some(crate::TransformedLine {
segments: vec![
(">>".to_string(), Some(Style::new().fg(reovim_arch::Color::Red))),
("replaced".to_string(), None),
],
})
}
}
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["original text".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(TransformModule)];
let token_provider = MockTokenProvider::empty();
let viewport = Rect::new(0, 0, 80, 24);
render_buffer_content(&mut surface, viewport, &ctx, &modules, &token_provider, &MockTheme);
let text = surface.text_at(0);
assert_eq!(text, ">>replaced", "buffer content should use transformed line");
}
#[test]
fn render_line_content_bg_token_zero_opacity() {
struct BgModule;
impl ClientModule for BgModule {
fn id(&self) -> &'static str {
"bg"
}
fn name(&self) -> &'static str {
"Bg"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "bg.token" {
Some(RenderBehavior::Background(reovim_arch::Color::Red))
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 5,
category: "bg.token".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(BgModule)];
render_line_content(
&mut surface,
0,
0,
80,
"hello",
0.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello");
}
#[test]
fn render_line_content_conceal_tokens_skip_all_types() {
struct AllBehaviorModule;
impl ClientModule for AllBehaviorModule {
fn id(&self) -> &'static str {
"all-behavior"
}
fn name(&self) -> &'static str {
"AllBehavior"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
match category {
"conceal" => Some(RenderBehavior::Conceal {
replacement: Cow::Borrowed("X"),
}),
"hidden" => Some(RenderBehavior::Hide),
"hr" => Some(RenderBehavior::FullWidthLine {
ch: '-',
style: Style::default(),
}),
_ => None,
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![
SyntaxToken {
line: 0,
start_col: 0,
end_col: 2,
category: "conceal".to_owned(),
},
SyntaxToken {
line: 0,
start_col: 3,
end_col: 5,
category: "hidden".to_owned(),
},
SyntaxToken {
line: 0,
start_col: 6,
end_col: 8,
category: "hr".to_owned(),
},
];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(AllBehaviorModule)];
render_line_content(
&mut surface,
0,
0,
80,
"ab cd efgh",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
true, &modules,
);
let text = surface.text_at(0);
assert_eq!(text, "ab cd efgh", "all conceals should be skipped in insert mode");
}
#[test]
fn render_remote_cursors_col_beyond_width() {
let mut surface = RecordingSurface::new(20, 24);
let lines = vec!["hello".to_string()];
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 0,
cursor_col: 100, mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursors(&mut surface, &ctx, 0, 24);
}
#[test]
fn render_remote_cursors_past_content_height() {
let mut surface = RecordingSurface::new(80, 24);
let remote = crate::RemoteClientInfo {
client_id: 1,
display_name: "Alice".to_string(),
cursor_line: 30,
cursor_col: 0,
mode: "normal".to_string(),
cursor_color: reovim_arch::Color::Blue,
selection: None,
};
let remotes = vec![remote];
let ctx = ViewportContext {
remote_clients: &remotes,
..empty_ctx()
};
render_remote_cursors(&mut surface, &ctx, 0, 5);
}
#[test]
fn render_self_cursor_past_content_height() {
let mut surface = RecordingSurface::new(80, 24);
let ctx = ViewportContext {
cursor: Some(CursorInfo {
line: 50,
column: 0,
}),
render_self_cursor: true,
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 5, &tokens, &MockTheme, &modules);
}
#[test]
fn render_self_cursor_col_beyond_width() {
let mut surface = RecordingSurface::new(10, 24);
let lines = vec!["hello".to_string()];
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
cursor: Some(CursorInfo {
line: 0,
column: 100,
}),
render_self_cursor: true,
is_insert_mode: true, ..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
let tokens = MockTokenProvider::empty();
render_self_cursor(&mut surface, &ctx, 0, 24, &tokens, &MockTheme, &modules);
}
#[test]
fn render_selection_char_multiline_no_buffer_id() {
let mut surface = RecordingSurface::new(80, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 2,
end_line: 1,
end_col: 3,
mode: crate::SelectionMode::Char,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: None, buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 7, "multi-line char selection without buffer_id");
}
#[test]
fn render_line_content_conceal_with_style_and_unmapped_cols() {
struct ConcealModule;
impl ClientModule for ConcealModule {
fn id(&self) -> &'static str {
"conceal"
}
fn name(&self) -> &'static str {
"Conceal"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "conceal.url" {
Some(RenderBehavior::Conceal {
replacement: Cow::Borrowed("LINK"),
})
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![
SyntaxToken {
line: 0,
start_col: 0,
end_col: 2,
category: "keyword".to_owned(),
},
SyntaxToken {
line: 0,
start_col: 2,
end_col: 7,
category: "conceal.url".to_owned(),
},
];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(ConcealModule)];
render_line_content(
&mut surface,
0,
0,
80,
"abcdefghi",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert!(text.contains("LINK"), "conceal replacement should appear: {text}");
assert!(text.contains("ab"), "prefix should remain: {text}");
assert!(text.contains("hi"), "suffix should remain: {text}");
}
#[test]
fn render_line_content_bg_token_end_beyond_line() {
struct BgModule;
impl ClientModule for BgModule {
fn id(&self) -> &'static str {
"bg"
}
fn name(&self) -> &'static str {
"Bg"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "bg.token" {
Some(RenderBehavior::Background(reovim_arch::Color::Red))
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 100, category: "bg.token".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(BgModule)];
render_line_content(
&mut surface,
0,
0,
80,
"hi",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 2, "bg overlay clamped to line length");
}
#[test]
fn render_line_content_bg_token_col_beyond_width() {
struct BgModule;
impl ClientModule for BgModule {
fn id(&self) -> &'static str {
"bg"
}
fn name(&self) -> &'static str {
"Bg"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn classify_token(&self, category: &str) -> Option<RenderBehavior> {
if category == "bg.token" {
Some(RenderBehavior::Background(reovim_arch::Color::Red))
} else {
None
}
}
}
let mut surface = RecordingSurface::new(80, 24);
let tokens = vec![SyntaxToken {
line: 0,
start_col: 0,
end_col: 5,
category: "bg.token".to_owned(),
}];
let token_provider = MockTokenProvider::with_tokens(tokens);
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(BgModule)];
render_line_content(
&mut surface,
0,
0,
3, "hello",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 3, "bg overlay clamped to content width");
}
#[test]
fn render_line_content_inline_dec_clamped_by_width() {
struct InlineDecModule {
decorations: Vec<crate::InlineDecoration>,
}
impl ClientModule for InlineDecModule {
fn id(&self) -> &'static str {
"inline-dec"
}
fn name(&self) -> &'static str {
"InlineDec"
}
fn version(&self) -> crate::Version {
crate::Version::new(1, 0, 0)
}
fn init(&mut self, _ctx: &crate::ModuleContext) -> crate::ProbeResult {
crate::ProbeResult::Success
}
fn exit(&mut self) -> Result<(), crate::ClientModuleError> {
Ok(())
}
fn has_buffer_contrib(&self) -> bool {
true
}
fn inline_decorations(&self, _line: usize) -> &[crate::InlineDecoration] {
&self.decorations
}
}
let mut surface = RecordingSurface::new(80, 24);
let module = InlineDecModule {
decorations: vec![crate::InlineDecoration {
col_start: 0,
col_end: 100, style: Style::new().fg(reovim_arch::Color::Red),
}],
};
let modules: Vec<Box<dyn ClientModule>> = vec![Box::new(module)];
let token_provider = MockTokenProvider::empty();
render_line_content(
&mut surface,
0,
0,
5, "hello world",
1.0,
Some(BufferId(0)),
0,
&token_provider,
&MockTheme,
false,
&modules,
);
let text = surface.text_at(0);
assert_eq!(text, "hello", "content should be truncated to width");
}
#[test]
fn render_positioned_vl_past_content_height() {
let mut surface = RecordingSurface::new(80, 2);
let vlines = vec![
VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::After,
content: "after1".to_string(),
style: Style::default(),
},
VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::After,
content: "after2".to_string(),
style: Style::default(),
},
VirtualLine {
buffer_line: 0,
position: VirtualLinePosition::After,
content: "after3".to_string(),
style: Style::default(),
},
];
let ctx = ViewportContext {
virtual_lines: &vlines,
..empty_ctx()
};
let rows = render_positioned_virtual_lines(
&mut surface,
&ctx,
0,
0,
1,
80,
2,
0,
VirtualLinePosition::After,
);
assert_eq!(rows, 1, "only 1 VL should fit within remaining viewport height");
}
#[test]
fn dummy_caps_coverage() {
let caps = DummyCaps;
assert_eq!(caps.rendering_model(), crate::RenderingModel::CellGrid);
assert_eq!(caps.grid_size(), None);
assert_eq!(caps.color_depth(), crate::ColorDepth::TrueColor);
assert_eq!(caps.pixel_size(), None);
assert!(caps.reliable_unicode_width());
assert!(caps.dark_mode());
assert!(!caps.smooth_scroll());
assert!(!caps.pointer_events());
assert!(!caps.touch_input());
assert!(!caps.haptic());
assert_eq!(caps.safe_area(), crate::Insets::ZERO);
assert!(caps.has_focus());
assert!(caps.clipboard_available());
assert!(!caps.screen_reader_active());
}
#[test]
fn render_selection_line_mode_multi_line() {
let mut surface = RecordingSurface::new(20, 24);
let lines = vec!["hello".to_string(), "world".to_string()];
let local_sel = SelectionInfo {
start_line: 0,
start_col: 0,
end_line: 1,
end_col: 0,
mode: crate::SelectionMode::Line,
color: reovim_arch::Color::Blue,
};
let ctx = ViewportContext {
buffer_id: Some(BufferId(0)),
buffer_lines: Some(&lines),
local_selection: Some(local_sel),
..empty_ctx()
};
let modules: Vec<Box<dyn ClientModule>> = Vec::new();
render_selections(&mut surface, &ctx, 0, 24, &modules);
let overlays = surface.overlays.borrow();
assert_eq!(overlays.len(), 40, "Line mode multi-line selection");
}