feat: enlarge floating hover and highlight Markdown content

This commit is contained in:
2026-10-10 16:10:25 +02:00
parent 99028daa9d
commit dc0bdd60d0
5 changed files with 123 additions and 15 deletions
+108 -12
View File
@@ -567,16 +567,7 @@ fn draw(
spans
.get(span_index)
.filter(|span| span.start_byte <= byte)
.map(|span| {
Style::default().fg(match span.kind {
SyntaxKind::Comment => Color::DarkGray,
SyntaxKind::String => Color::Green,
SyntaxKind::Keyword => Color::Magenta,
SyntaxKind::Number => Color::Yellow,
SyntaxKind::Type | SyntaxKind::Markup => Color::Cyan,
SyntaxKind::Function => Color::LightBlue,
})
})
.map(|span| syntax_style(span.kind))
});
if let Some(style) = style
&& end > *left
@@ -907,6 +898,7 @@ fn draw_startup_diagnostic(frame: &mut Frame, diagnostic: &StartupDiagnostic, ar
#[derive(Default)]
struct WheelPanels {
hover: Option<(Rect, usize)>,
hover_syntax: SyntaxCache,
files: Option<(Rect, usize, usize)>,
manual_files: Option<FileWheelOffset>,
}
@@ -1037,7 +1029,7 @@ fn draw_hover(frame: &mut Frame, hover: &Hover, area: Rect, panels: &mut WheelPa
}
let [_, column, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(area.width.min(84)),
Constraint::Length(area.width.min(100)),
Constraint::Fill(1),
])
.areas(area);
@@ -1045,7 +1037,7 @@ fn draw_hover(frame: &mut Frame, hover: &Hover, area: Rect, panels: &mut WheelPa
Constraint::Fill(1),
Constraint::Length(
area.height
.min(hover.lines().len().saturating_add(3).min(18) as u16),
.min(hover.lines().len().saturating_add(3).min(24) as u16),
),
Constraint::Fill(1),
])
@@ -1083,6 +1075,50 @@ fn draw_hover(frame: &mut Frame, hover: &Hover, area: Rect, panels: &mut WheelPa
})
.collect();
frame.render_widget(Paragraph::new(lines), text_area);
panels
.hover_syntax
.update_text(hover.text(), std::path::Path::new("hover.md"));
let width = usize::from(text_area.width);
for (row, line) in hover
.lines()
.enumerate()
.skip(hover.top)
.take(usize::from(text_area.height))
{
let spans = panels.hover_syntax.line(row);
if spans.is_empty() {
continue;
}
let left = display_column(line, hover.left);
let right = left.saturating_add(width);
let mut column = 0;
let mut span_index = 0;
for (byte, grapheme) in line.grapheme_indices(true) {
if column >= right {
break;
}
let cells = if grapheme == "\t" {
TAB_WIDTH - column % TAB_WIDTH
} else {
UnicodeWidthStr::width(grapheme)
};
let end = column + cells;
while span_index < spans.len() && spans[span_index].end_byte <= byte {
span_index += 1;
}
if let Some(span) = spans.get(span_index).filter(|span| span.start_byte <= byte)
&& end > left
&& (grapheme == "\t" || (column >= left && end <= right))
{
let style = syntax_style(span.kind);
let y = text_area.y + (row - hover.top) as u16;
for cell in column.max(left)..end.min(right) {
frame.buffer_mut()[(text_area.x + (cell - left) as u16, y)].set_style(style);
}
}
column = end;
}
}
frame.render_widget(
Paragraph::new("Arrows/Ctrl+h/j/k/l scroll · Esc close")
.style(Style::default().fg(Color::DarkGray)),
@@ -1401,6 +1437,17 @@ fn draw_palette(frame: &mut Frame, palette: &Palette, mode: Mode, navigation_hin
);
}
fn syntax_style(kind: SyntaxKind) -> Style {
Style::default().fg(match kind {
SyntaxKind::Comment => Color::DarkGray,
SyntaxKind::String => Color::Green,
SyntaxKind::Keyword => Color::Magenta,
SyntaxKind::Number => Color::Yellow,
SyntaxKind::Type | SyntaxKind::Markup => Color::Cyan,
SyntaxKind::Function => Color::LightBlue,
})
}
fn semantic_style(token: &SemanticToken) -> Style {
use SemanticTokenKind::*;
let readonly = token.modifiers & SemanticTokenModifier::Readonly as u16 != 0;
@@ -1482,6 +1529,55 @@ mod tests {
.join("\n")
}
#[test]
fn hover_shows_more_documentation_with_markdown_and_fenced_code_colors() {
let mut editor = Editor::open(None, vec![]).unwrap();
let title = format!("# {}TAIL", "Heading ".repeat(11));
let details = (5..36)
.map(|row| format!("detail-{row:02}"))
.collect::<Vec<_>>()
.join("\n");
editor
.show_hover(format!(
"{title}\n\n```rust\nfn answer() -> i32 {{ 42 }}\n```\n{details}"
))
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 32)).unwrap();
let mut panels = WheelPanels::default();
terminal
.draw(|frame| draw_hover(frame, editor.hover().unwrap(), frame.area(), &mut panels))
.unwrap();
assert!(screen_text(&terminal).contains("TAIL"));
assert!(screen_text(&terminal).contains("detail-20"));
let (panel, _) = panels.hover.unwrap();
let (x, y) = (panel.x + 1, panel.y + 1);
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(x, y)].fg, Color::Cyan);
assert_eq!(buffer[(x, y + 3)].symbol(), "f");
assert_eq!(buffer[(x, y + 3)].fg, Color::Magenta);
assert_eq!(buffer[(x + 6, y + 3)].fg, Color::LightBlue);
assert_eq!(buffer[(x + 21, y + 3)].fg, Color::Yellow);
for _ in 0..3 {
editor.handle(Input::Down);
}
editor.handle(Input::Right);
terminal
.draw(|frame| draw_hover(frame, editor.hover().unwrap(), frame.area(), &mut panels))
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(x, y)].symbol(), "n");
assert_eq!(buffer[(x, y)].fg, Color::Magenta);
assert_eq!(buffer[(x + 20, y)].fg, Color::Yellow);
editor.show_hover("# Replacement".into()).unwrap();
terminal
.draw(|frame| draw_hover(frame, editor.hover().unwrap(), frame.area(), &mut panels))
.unwrap();
assert!(screen_text(&terminal).contains("# Replacement"));
assert!(!screen_text(&terminal).contains("answer"));
}
#[test]
fn hover_scroll_renders_whole_graphemes_in_tiny_workspace() {
let mut editor = Editor::open(None, vec![]).unwrap();