use crate::{
BufferId, ClientModule, ConcealDecoration, CursorInfo, PlatformCapabilities, Rect,
RenderBehavior, RenderSurface, SelectionInfo, SelectionMode, Style, ThemeProvider,
TokenProvider, TransformedLine, ViewportContext, VirtualLinePosition,
conceal::{apply_conceals, dim_style, source_to_display_col},
};
const DEFAULT_BG: reovim_arch::Color = reovim_arch::Color::Black;
pub struct DefaultViewportRenderer;
impl crate::ViewportRenderer for DefaultViewportRenderer {
#[cfg_attr(coverage_nightly, coverage(off))]
fn gutter_width(
&self,
modules: &[Box<dyn ClientModule>],
caps: &dyn PlatformCapabilities,
) -> u16 {
let ctx = crate::AnnotationContext {
buffer_id: BufferId(0),
total_lines: 0,
visible_range: (0, 0),
cursor_line: 0,
gutter_style: Style::default(),
};
modules
.iter()
.filter(|m| m.has_annotations())
.map(|m| match m.annotation_column_width(&ctx, caps) {
crate::ColumnWidth::Fixed(w) => w,
crate::ColumnWidth::Dynamic(min) => min,
})
.sum()
}
#[allow(clippy::too_many_arguments)]
fn render_viewport(
&self,
surface: &mut dyn RenderSurface,
viewport: Rect,
ctx: &ViewportContext<'_>,
modules: &[Box<dyn ClientModule>],
tokens: &dyn TokenProvider,
theme: &dyn ThemeProvider,
_caps: &dyn PlatformCapabilities,
) {
let content_x = viewport.x + ctx.sidebar_width + ctx.gutter_width;
let content_height = viewport.height;
render_buffer_content(surface, viewport, ctx, modules, tokens, theme);
render_selections(surface, ctx, content_x, content_height, modules);
render_remote_cursors(surface, ctx, content_x, content_height);
if ctx.render_self_cursor {
render_self_cursor(surface, ctx, content_x, content_height, tokens, theme, modules);
}
}
}
#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
fn render_buffer_content(
surface: &mut dyn RenderSurface,
viewport: Rect,
ctx: &ViewportContext<'_>,
modules: &[Box<dyn ClientModule>],
tokens: &dyn TokenProvider,
theme: &dyn ThemeProvider,
) {
let content_x = viewport.x + ctx.sidebar_width + ctx.gutter_width;
let content_width = viewport
.width
.saturating_sub(ctx.sidebar_width + ctx.gutter_width);
let content_height = viewport.height;
let cursor_line = ctx.cursor.map(|c| c.line as usize);
let mut screen_row: u16 = 0;
let mut line_idx = ctx.scroll_top;
while screen_row < content_height {
if is_line_folded(line_idx, ctx.fold_ranges) {
line_idx += 1;
continue;
}
screen_row += render_positioned_virtual_lines(
surface,
ctx,
content_x,
viewport.y,
screen_row,
content_width,
content_height,
line_idx,
VirtualLinePosition::Before,
);
if screen_row >= content_height {
break;
}
if ctx.gutter_width > 0 {
render_gutter_annotations(
surface,
viewport.x + ctx.sidebar_width,
viewport.y + screen_row,
ctx.gutter_width,
line_idx,
modules,
ctx,
);
}
if let Some(lines) = ctx.buffer_lines {
if line_idx < lines.len() {
let line = &lines[line_idx];
let transform = transform_line(modules, ctx.buffer_id, line_idx, line);
if let Some(ref transformed) = transform {
render_transformed_line(
surface,
content_x,
viewport.y + screen_row,
content_width,
transformed,
);
} else {
render_line_content(
surface,
content_x,
viewport.y + screen_row,
content_width,
line,
ctx.opacity,
ctx.buffer_id,
line_idx,
tokens,
theme,
ctx.is_insert_mode && cursor_line == Some(line_idx),
modules,
);
}
} else {
let tilde_style =
apply_opacity(&Style::new().fg(reovim_arch::Color::DarkGrey), ctx.opacity);
let mut buf = [0u8; 4];
let s = '~'.encode_utf8(&mut buf);
surface.write_styled(content_x, viewport.y + screen_row, s, tilde_style);
}
}
screen_row += 1;
screen_row += render_positioned_virtual_lines(
surface,
ctx,
content_x,
viewport.y,
screen_row,
content_width,
content_height,
line_idx,
VirtualLinePosition::After,
);
line_idx += 1;
}
}
#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
fn render_positioned_virtual_lines(
surface: &mut dyn RenderSurface,
ctx: &ViewportContext<'_>,
content_x: u16,
viewport_y: u16,
screen_row: u16,
content_width: u16,
content_height: u16,
line_idx: usize,
position: VirtualLinePosition,
) -> u16 {
let mut rows_used: u16 = 0;
for vl in ctx
.virtual_lines
.iter()
.filter(|vl| vl.buffer_line == line_idx && vl.position == position)
{
if screen_row + rows_used >= content_height {
break;
}
render_virtual_line(
surface,
content_x,
viewport_y + screen_row + rows_used,
content_width,
&vl.content,
&vl.style,
);
rows_used += 1;
}
rows_used
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
fn render_line_content(
surface: &mut dyn RenderSurface,
x: u16,
y: u16,
width: u16,
line: &str,
opacity: f32,
buffer_id: Option<BufferId>,
line_idx: usize,
token_provider: &dyn TokenProvider,
theme: &dyn ThemeProvider,
skip_conceals: bool,
modules: &[Box<dyn ClientModule>],
) {
let tokens = buffer_id
.map(|bid| token_provider.tokens_for_line(bid, line_idx as u32))
.unwrap_or_default();
let default_style = apply_opacity(&Style::default(), opacity);
let mut conceals: Vec<ConcealDecoration> = Vec::new();
let mut backgrounds: Vec<(u32, u32, Style)> = Vec::new();
for t in &tokens {
let behavior = classify_with_modules(modules, &t.category);
match behavior {
RenderBehavior::Conceal { replacement } if !skip_conceals => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: Some(replacement.into_owned()),
style: Some(theme.highlight(&t.category)),
});
}
RenderBehavior::Hide if !skip_conceals => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: None,
style: None,
});
}
RenderBehavior::FullWidthLine { ch, .. } if !skip_conceals => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: Some(ch.to_string().repeat(width as usize)),
style: Some(theme.highlight(&t.category)),
});
}
RenderBehavior::Background(_) => {
backgrounds.push((t.start_col, t.end_col, theme.highlight(&t.category)));
}
_ => {} }
}
let concealed = apply_conceals(line, &conceals);
for (col_offset, (display_col, ch)) in (0_u16..).zip(concealed.text.chars().enumerate()) {
if col_offset >= width {
break;
}
let style = if let Some(Some(conceal_style)) = concealed.styles.get(display_col) {
apply_opacity(conceal_style, opacity)
} else {
let source_col = concealed.col_mapping.get(display_col).copied().unwrap_or(0);
let source_col = u32::from(source_col);
tokens
.iter()
.filter(|t| {
matches!(classify_with_modules(modules, &t.category), RenderBehavior::Highlight)
&& source_col >= t.start_col
&& source_col < t.end_col
})
.min_by_key(|t| t.end_col - t.start_col)
.map_or_else(
|| default_style.clone(),
|t| apply_opacity(&theme.highlight(&t.category), opacity),
)
};
let mut buf = [0u8; 4];
let s = ch.encode_utf8(&mut buf);
surface.write_styled(x + col_offset, y, s, style);
}
let line_char_count = line.chars().count() as u32;
for (start_col, end_col, bg_style) in &backgrounds {
let bg = apply_opacity(bg_style, opacity);
for source_col in *start_col..(*end_col).min(line_char_count) {
let display_col = source_to_display_col(&concealed, source_col as usize);
#[allow(clippy::cast_possible_truncation)]
let col_u16 = display_col as u16;
if col_u16 < width
&& let Some(bg_color) = bg.bg
{
surface.overlay_bg(x + col_u16, y, bg_color);
}
}
}
for module in modules.iter().filter(|m| m.has_buffer_contrib()) {
for dec in module.inline_decorations(line_idx) {
for col in dec.col_start..dec.col_end.min(width) {
let dec_style = apply_opacity(&dec.style, opacity);
surface.apply_style(x + col, y, dec_style);
}
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn render_virtual_line(
surface: &mut dyn RenderSurface,
x: u16,
y: u16,
width: u16,
content: &str,
style: &Style,
) {
for (col, ch) in content.chars().enumerate() {
let col_u16 = col as u16;
if col_u16 >= width {
break;
}
let mut buf = [0u8; 4];
let s = ch.encode_utf8(&mut buf);
surface.write_styled(x + col_u16, y, s, style.clone());
}
}
#[allow(clippy::cast_possible_truncation)]
fn render_transformed_line(
surface: &mut dyn RenderSurface,
x: u16,
y: u16,
width: u16,
transformed: &TransformedLine,
) {
let mut col: u16 = 0;
for (seg_text, seg_style) in &transformed.segments {
for ch in seg_text.chars() {
if col >= width {
return;
}
let style = seg_style.clone().unwrap_or_default();
let mut buf = [0u8; 4];
let s = ch.encode_utf8(&mut buf);
surface.write_styled(x + col, y, s, style);
col += 1;
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::cast_possible_truncation)]
fn render_gutter_annotations(
surface: &mut dyn RenderSurface,
x: u16,
y: u16,
_total_width: u16,
line_idx: usize,
modules: &[Box<dyn ClientModule>],
ctx: &ViewportContext<'_>,
) {
let ann_ctx = crate::AnnotationContext {
buffer_id: ctx.buffer_id.unwrap_or(BufferId(0)),
total_lines: ctx.buffer_lines.map_or(0, <[String]>::len),
visible_range: (ctx.scroll_top, ctx.scroll_top + 100),
cursor_line: ctx.cursor.map_or(0, |c| c.line as usize),
gutter_style: Style::default(),
};
let mut ann_modules: Vec<&Box<dyn ClientModule>> =
modules.iter().filter(|m| m.has_annotations()).collect();
ann_modules.sort_by_key(|m| std::cmp::Reverse(m.annotation_priority()));
let caps = DummyCaps;
let mut col_x = x;
for module in &ann_modules {
let col_width = match module.annotation_column_width(&ann_ctx, &caps) {
crate::ColumnWidth::Fixed(w) => w,
crate::ColumnWidth::Dynamic(min) => min,
};
if col_width == 0 {
continue;
}
if let Some(cell) = module.annotate(line_idx, &ann_ctx) {
let style = apply_opacity(&cell.style, ctx.opacity);
let padding = (col_width as usize).saturating_sub(cell.text.len() + 1);
let display = format!("{:>width$} ", cell.text, width = padding + cell.text.len());
surface.write_styled(col_x, y, &display, style);
}
col_x += col_width;
}
}
struct DummyCaps;
impl PlatformCapabilities for DummyCaps {
fn rendering_model(&self) -> crate::RenderingModel {
crate::RenderingModel::CellGrid
}
fn grid_size(&self) -> Option<(u16, u16)> {
None
}
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
}
}
#[must_use]
pub fn is_line_folded(line: usize, fold_ranges: &[(usize, usize)]) -> bool {
for &(start, count) in fold_ranges {
if line >= start && line < start + count {
return true;
}
}
false
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn buffer_to_screen_row_vl(
buffer_line: u64,
scroll_top: u64,
virtual_lines: &[crate::VirtualLine],
) -> u64 {
let count = virtual_lines
.iter()
.filter(|vl| {
(vl.buffer_line as u64) >= scroll_top && (vl.buffer_line as u64) <= buffer_line
})
.count();
(buffer_line - scroll_top) + count as u64
}
fn apply_opacity(style: &Style, opacity: f32) -> Style {
if (opacity - 1.0).abs() < f32::EPSILON {
return style.clone();
}
dim_style(style, opacity, DEFAULT_BG)
}
fn classify_with_modules(modules: &[Box<dyn ClientModule>], category: &str) -> RenderBehavior {
modules
.iter()
.filter(|m| m.has_buffer_contrib())
.find_map(|m| m.classify_token(category))
.unwrap_or(RenderBehavior::Highlight)
}
fn transform_line(
modules: &[Box<dyn ClientModule>],
buffer_id: Option<BufferId>,
line_idx: usize,
text: &str,
) -> Option<TransformedLine> {
let bid = buffer_id?;
modules
.iter()
.filter(|m| m.has_buffer_contrib())
.find_map(|m| m.transform_line(bid, line_idx, text))
}
pub const CBF8_PALETTE: [reovim_arch::Color; 8] = [
reovim_arch::Color::Rgb {
r: 0,
g: 114,
b: 178,
}, reovim_arch::Color::Rgb {
r: 230,
g: 159,
b: 0,
}, reovim_arch::Color::Rgb {
r: 86,
g: 180,
b: 233,
}, reovim_arch::Color::Rgb {
r: 0,
g: 158,
b: 115,
}, reovim_arch::Color::Rgb {
r: 240,
g: 228,
b: 66,
}, reovim_arch::Color::Rgb {
r: 213,
g: 94,
b: 0,
}, reovim_arch::Color::Rgb {
r: 204,
g: 121,
b: 167,
}, reovim_arch::Color::Rgb { r: 0, g: 0, b: 0 }, ];
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn client_color(client_id: u64) -> reovim_arch::Color {
CBF8_PALETTE[(client_id as usize) % CBF8_PALETTE.len()]
}
pub const CBF8_DIMMED: [reovim_arch::Color; 8] = [
reovim_arch::Color::Rgb { r: 0, g: 45, b: 70 }, reovim_arch::Color::Rgb { r: 75, g: 50, b: 0 }, reovim_arch::Color::Rgb {
r: 25,
g: 60,
b: 75,
}, reovim_arch::Color::Rgb { r: 0, g: 55, b: 35 }, reovim_arch::Color::Rgb {
r: 70,
g: 65,
b: 20,
}, reovim_arch::Color::Rgb { r: 70, g: 30, b: 0 }, reovim_arch::Color::Rgb {
r: 65,
g: 38,
b: 55,
}, reovim_arch::Color::Rgb {
r: 30,
g: 30,
b: 30,
}, ];
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn dimmed_client_color(client_id: u64) -> reovim_arch::Color {
CBF8_DIMMED[(client_id as usize) % CBF8_DIMMED.len()]
}
pub const LOCAL_SELECTION_BG: reovim_arch::Color = reovim_arch::Color::Rgb {
r: 50,
g: 50,
b: 100,
};
const MAX_LABEL_WIDTH: usize = 16;
#[allow(clippy::cast_possible_truncation)]
fn render_selections(
surface: &mut dyn RenderSurface,
ctx: &ViewportContext<'_>,
content_x: u16,
content_height: u16,
modules: &[Box<dyn ClientModule>],
) {
let (width, _) = surface.size();
let content_width = width.saturating_sub(content_x);
let scroll_top = ctx.scroll_top as u64;
for remote in ctx.remote_clients {
if let Some(sel) = &remote.selection {
render_selection_range(
surface,
sel,
content_x,
content_height,
content_width,
ctx.buffer_lines,
scroll_top,
ctx.virtual_lines,
modules,
ctx.buffer_id,
);
}
}
if let Some(sel) = &ctx.local_selection {
render_selection_range(
surface,
sel,
content_x,
content_height,
content_width,
ctx.buffer_lines,
scroll_top,
ctx.virtual_lines,
modules,
ctx.buffer_id,
);
}
}
#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
fn render_selection_range(
surface: &mut dyn RenderSurface,
sel: &SelectionInfo,
content_x: u16,
content_height: u16,
content_width: u16,
lines: Option<&[String]>,
scroll_top: u64,
virtual_lines: &[crate::VirtualLine],
modules: &[Box<dyn ClientModule>],
buffer_id: Option<BufferId>,
) {
let (start_line, start_col, end_line, end_col) = normalize_selection(sel);
for line in start_line..=end_line {
if line < scroll_top {
continue;
}
let screen_line = buffer_to_screen_row_vl(line, scroll_top, virtual_lines);
if screen_line as u16 >= content_height {
break;
}
let line_idx = line as usize;
let map_col = |buf_col: u64| -> u16 {
buffer_id
.and_then(|bid| {
modules
.iter()
.filter(|m| m.has_buffer_contrib())
.find_map(|m| m.map_cursor_column(bid, line_idx, buf_col as usize))
})
.unwrap_or(buf_col as u16)
};
let visual_line_len = buffer_id
.and_then(|bid| {
modules
.iter()
.filter(|m| m.has_buffer_contrib())
.find_map(|m| {
let text = lines
.and_then(|l| l.get(line_idx))
.map_or("", String::as_str);
m.transform_line(bid, line_idx, text)
})
})
.map_or_else(
|| {
lines
.and_then(|l| l.get(line_idx))
.map_or(content_width, |s| s.len() as u16)
},
|t| {
t.segments
.iter()
.map(|(s, _)| s.chars().count())
.sum::<usize>() as u16
},
);
let (col_start, col_end) = match sel.mode {
SelectionMode::Line => (0u16, content_width),
SelectionMode::Block => {
(map_col(start_col), (map_col(end_col) + 1).min(visual_line_len))
}
SelectionMode::Char => {
if start_line == end_line {
(map_col(start_col), (map_col(end_col) + 1).min(visual_line_len))
} else if line == start_line {
(map_col(start_col), visual_line_len)
} else if line == end_line {
(0, (map_col(end_col) + 1).min(visual_line_len))
} else {
(0, visual_line_len)
}
}
};
#[allow(clippy::cast_possible_truncation)]
let screen_y = screen_line as u16;
for col in col_start..col_end.min(content_width) {
surface.overlay_bg(content_x + col, screen_y, sel.color);
}
}
}
fn normalize_selection(sel: &SelectionInfo) -> (u64, u64, u64, u64) {
if (sel.start_line, sel.start_col) <= (sel.end_line, sel.end_col) {
(sel.start_line, sel.start_col, sel.end_line, sel.end_col)
} else {
(sel.end_line, sel.end_col, sel.start_line, sel.start_col)
}
}
#[allow(clippy::cast_possible_truncation)]
fn render_remote_cursors(
surface: &mut dyn RenderSurface,
ctx: &ViewportContext<'_>,
content_x: u16,
content_height: u16,
) {
let (width, _) = surface.size();
let scroll_top = ctx.scroll_top as u64;
for remote in ctx.remote_clients {
if remote.cursor_line < scroll_top {
continue;
}
let screen_line =
buffer_to_screen_row_vl(remote.cursor_line, scroll_top, ctx.virtual_lines);
let screen_col = remote.cursor_col as u16 + content_x;
if screen_line < u64::from(content_height) && screen_col < width {
let cursor_style = Style::new()
.bg(remote.cursor_color)
.fg(reovim_arch::Color::White);
let screen_y = screen_line as u16;
surface.apply_style(screen_col, screen_y, cursor_style);
}
}
render_remote_cursor_labels(surface, ctx, content_x, content_height);
}
#[allow(clippy::cast_possible_truncation)]
fn render_remote_cursor_labels(
surface: &mut dyn RenderSurface,
ctx: &ViewportContext<'_>,
content_x: u16,
content_height: u16,
) {
let (width, _) = surface.size();
let scroll_top = ctx.scroll_top as u64;
for remote in ctx.remote_clients {
if remote.cursor_line < scroll_top {
continue;
}
let screen_line =
buffer_to_screen_row_vl(remote.cursor_line, scroll_top, ctx.virtual_lines);
if screen_line >= u64::from(content_height) {
continue;
}
let screen_y = screen_line as u16;
let eol_col = ctx
.buffer_lines
.and_then(|l| l.get(remote.cursor_line as usize))
.map_or(0, |line| crate::ui::display_width(line) as u16);
let label_x = content_x + eol_col + 1;
let label = label_text(&remote.display_name, &remote.mode);
let label_width = crate::ui::display_width(&label) as u16;
if label_x + label_width > width {
continue;
}
let label_style = Style::new().fg(remote.cursor_color).underline();
surface.write_styled(label_x, screen_y, &label, label_style);
}
}
#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
fn render_self_cursor(
surface: &mut dyn RenderSurface,
ctx: &ViewportContext<'_>,
content_x: u16,
content_height: u16,
tokens: &dyn TokenProvider,
theme: &dyn ThemeProvider,
modules: &[Box<dyn ClientModule>],
) {
let Some(cursor) = ctx.cursor else {
return;
};
let scroll_top = ctx.scroll_top as u64;
if cursor.line < scroll_top {
return;
}
let (width, _) = surface.size();
let screen_line = buffer_to_screen_row_vl(cursor.line, scroll_top, ctx.virtual_lines);
let visual_col = ctx
.buffer_id
.and_then(|bid| {
modules
.iter()
.filter(|m| m.has_buffer_contrib())
.find_map(|m| {
m.map_cursor_column(bid, cursor.line as usize, cursor.column as usize)
})
})
.unwrap_or_else(|| {
if ctx.is_insert_mode {
cursor.column as u16
} else {
compute_cursor_visual_col(ctx, &cursor, tokens, theme, modules)
}
});
let screen_col = visual_col + content_x;
if screen_line < u64::from(content_height) && screen_col < width {
let cursor_style = Style::new()
.bg(reovim_arch::Color::White)
.fg(reovim_arch::Color::Black);
let screen_y = screen_line as u16;
surface.apply_style(screen_col, screen_y, cursor_style);
}
}
#[allow(clippy::cast_possible_truncation)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn compute_cursor_visual_col(
ctx: &ViewportContext<'_>,
cursor: &CursorInfo,
tokens: &dyn TokenProvider,
theme: &dyn ThemeProvider,
modules: &[Box<dyn ClientModule>],
) -> u16 {
let Some(bid) = ctx.buffer_id else {
return cursor.column as u16;
};
let line_content = ctx
.buffer_lines
.and_then(|lines| lines.get(cursor.line as usize));
let Some(line) = line_content else {
return cursor.column as u16;
};
let line_tokens = tokens.tokens_for_line(bid, cursor.line as u32);
let mut conceals: Vec<ConcealDecoration> = Vec::new();
for t in &line_tokens {
match classify_with_modules(modules, &t.category) {
RenderBehavior::Conceal { replacement } => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: Some(replacement.into_owned()),
style: Some(theme.highlight(&t.category)),
});
}
RenderBehavior::Hide => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: None,
style: None,
});
}
RenderBehavior::FullWidthLine { ch, .. } => {
conceals.push(ConcealDecoration {
start_col: t.start_col as usize,
end_col: t.end_col as usize,
replacement: Some(ch.to_string().repeat(500)),
style: Some(theme.highlight(&t.category)),
});
}
_ => {}
}
}
if conceals.is_empty() {
return cursor.column as u16;
}
let concealed = apply_conceals(line, &conceals);
source_to_display_col(&concealed, cursor.column as usize) as u16
}
fn label_text(display_name: &str, mode: &str) -> String {
let name = if display_name.is_empty() {
"?"
} else {
display_name
};
let mode_abbrev = mode_abbreviation(mode);
let name = crate::ui::truncate_end(name, MAX_LABEL_WIDTH);
format!(" {name} {mode_abbrev} ")
}
fn mode_abbreviation(mode: &str) -> &'static str {
let lower = mode.to_lowercase();
if lower.contains("insert") {
"[I]"
} else if lower.contains("visual") {
"[V]"
} else if lower.contains("command") || lower.contains("cmdline") {
"[C]"
} else if lower.contains("replace") {
"[R]"
} else {
"[N]"
}
}
#[cfg(test)]
#[path = "viewport_tests.rs"]
mod tests;