feat: add live-cycling LSP completion
This commit is contained in:
+163
-4
@@ -23,7 +23,7 @@ use crossterm::{
|
||||
use ratatui::{
|
||||
Frame, Terminal,
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
layout::{Constraint, Layout, Position, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
@@ -32,7 +32,9 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
editor::{Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic},
|
||||
editor::{
|
||||
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
|
||||
},
|
||||
files::{DockSide, FilePicker, FilePresentation, FileView},
|
||||
plugin::{
|
||||
Diagnostic, DiagnosticSeverity, SemanticToken, SemanticTokenKind, SemanticTokenModifier,
|
||||
@@ -446,6 +448,7 @@ fn draw(
|
||||
let height = usize::from(viewport.height);
|
||||
let width = usize::from(viewport.width);
|
||||
let cursor = editor.cursor();
|
||||
let mut document_caret = None;
|
||||
let carets = editor.carets();
|
||||
let selections = editor.selections();
|
||||
if document.manual.as_ref().is_some_and(|manual| {
|
||||
@@ -820,13 +823,20 @@ fn draw(
|
||||
&& column >= *left
|
||||
&& column < left.saturating_add(width)
|
||||
{
|
||||
frame.set_cursor_position((
|
||||
let position = Position::new(
|
||||
viewport.x + (column - *left) as u16,
|
||||
viewport.y + (cursor.row - *top) as u16,
|
||||
));
|
||||
);
|
||||
frame.set_cursor_position(position);
|
||||
document_caret = Some(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(completion) = editor.completion()
|
||||
&& let Some(caret) = document_caret
|
||||
{
|
||||
panels.completion = draw_completion(frame, completion, viewport, caret);
|
||||
}
|
||||
if let Some(picker) = editor.file_picker() {
|
||||
if picker.presentation == FilePresentation::Floating {
|
||||
let panel = floating_panel(area);
|
||||
@@ -916,6 +926,7 @@ fn draw_startup_diagnostic(frame: &mut Frame, diagnostic: &StartupDiagnostic, ar
|
||||
|
||||
#[derive(Default)]
|
||||
struct WheelPanels {
|
||||
completion: Option<Rect>,
|
||||
hover: Option<(Rect, usize)>,
|
||||
hover_syntax: SyntaxCache,
|
||||
files: Option<(Rect, usize, usize)>,
|
||||
@@ -952,6 +963,7 @@ impl WheelPanels {
|
||||
self.manual_files = None;
|
||||
}
|
||||
self.hover = None;
|
||||
self.completion = None;
|
||||
self.files = None;
|
||||
}
|
||||
|
||||
@@ -992,6 +1004,13 @@ impl WheelPanels {
|
||||
return true;
|
||||
}
|
||||
let position = ratatui::layout::Position::new(mouse.column, mouse.row);
|
||||
if editor.completion().is_some()
|
||||
&& self
|
||||
.completion
|
||||
.is_some_and(|panel| panel.contains(position))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(hover) = editor.hover()
|
||||
&& let Some((panel, height)) = self.hover
|
||||
&& panel.contains(position)
|
||||
@@ -1042,6 +1061,69 @@ impl WheelPanels {
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_completion(
|
||||
frame: &mut Frame,
|
||||
completion: &Completion,
|
||||
area: Rect,
|
||||
caret: Position,
|
||||
) -> Option<Rect> {
|
||||
if area.width == 0 || area.height == 0 || !area.contains(caret) {
|
||||
return None;
|
||||
}
|
||||
let count = completion.items.len().saturating_add(1);
|
||||
let selected = completion.selected.min(count - 1);
|
||||
let desired_height = count.min(8) as u16;
|
||||
let below = area.bottom() - caret.y - 1;
|
||||
let above = caret.y - area.y;
|
||||
let downward = below >= desired_height || below >= above;
|
||||
let height = desired_height.min(if downward { below } else { above }.max(1));
|
||||
let first = selected.saturating_sub(usize::from(height) - 1);
|
||||
let lines: Vec<Line> = (first..count)
|
||||
.take(usize::from(height))
|
||||
.map(|index| {
|
||||
let label = if index == 0 {
|
||||
"Original text".to_owned()
|
||||
} else {
|
||||
let item = &completion.items[index - 1];
|
||||
let mut label = display_text(&item.label);
|
||||
if let Some(detail) = &item.detail {
|
||||
label.push_str(" · ");
|
||||
label.push_str(&display_text(detail));
|
||||
}
|
||||
label
|
||||
};
|
||||
let style = if index == selected {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray).bg(Color::Black)
|
||||
};
|
||||
Line::styled(
|
||||
visible_line(&label, 0, usize::from(area.width.min(60))),
|
||||
style,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let width = lines.iter().map(Line::width).max().unwrap_or(1).max(1) as u16;
|
||||
let panel = Rect::new(
|
||||
caret.x.min(area.right() - width),
|
||||
if below == 0 && above == 0 {
|
||||
caret.y
|
||||
} else if downward {
|
||||
caret.y + 1
|
||||
} else {
|
||||
caret.y - height
|
||||
},
|
||||
width,
|
||||
height,
|
||||
);
|
||||
frame.render_widget(Clear, panel);
|
||||
frame.render_widget(
|
||||
Paragraph::new(lines).style(Style::default().fg(Color::Gray).bg(Color::Black)),
|
||||
panel,
|
||||
);
|
||||
Some(panel)
|
||||
}
|
||||
|
||||
fn draw_hover(frame: &mut Frame, hover: &Hover, area: Rect, panels: &mut WheelPanels) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
@@ -1548,6 +1630,83 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_windows_selected_unicode_without_overwriting_gutter_dock_or_cursor() {
|
||||
let mut completion = Completion {
|
||||
items: (0..20)
|
||||
.map(|index| crate::plugin::CompletionItem {
|
||||
label: if index == 19 {
|
||||
"界e\u{301}👩🚀\u{1b}".into()
|
||||
} else {
|
||||
format!("item-{index:02}")
|
||||
},
|
||||
detail: Some("\tTAIL".into()),
|
||||
primary_edit: 0,
|
||||
edits: vec![],
|
||||
})
|
||||
.collect(),
|
||||
selected: 20,
|
||||
};
|
||||
for (width, height) in [(14, 6), (9, 1), (5, 1)] {
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
let workspace = Rect::new(2, 0, width - 4, height);
|
||||
let caret = Position::new(2, 0);
|
||||
let mut panel = None;
|
||||
completion.selected = 20;
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let rows: Vec<Line> = (0..height)
|
||||
.map(|_| Line::from(format!("GG{}EE", ".".repeat(usize::from(width - 4)))))
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(rows), frame.area());
|
||||
frame.set_cursor_position(caret);
|
||||
panel = draw_completion(frame, &completion, workspace, caret);
|
||||
})
|
||||
.unwrap();
|
||||
let mut panel = panel.unwrap();
|
||||
let selected_y = panel.bottom() - 1;
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(panel.x, selected_y)].bg, Color::Cyan);
|
||||
assert!(!screen_text(&terminal).contains("item-00"));
|
||||
for y in 0..height {
|
||||
for x in [0, 1] {
|
||||
assert_eq!(buffer[(x, y)].symbol(), "G");
|
||||
}
|
||||
for x in [width - 2, width - 1] {
|
||||
assert_eq!(buffer[(x, y)].symbol(), "E");
|
||||
}
|
||||
}
|
||||
if workspace.width >= 5 {
|
||||
assert_eq!(buffer[(panel.x, selected_y)].symbol(), "界");
|
||||
assert_eq!(buffer[(panel.x + 2, selected_y)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(panel.x + 3, selected_y)].symbol(), "👩🚀");
|
||||
} else {
|
||||
assert_eq!(buffer[(panel.x, selected_y)].symbol(), " ");
|
||||
}
|
||||
if workspace.width >= 6 {
|
||||
assert_eq!(buffer[(panel.x + 5, selected_y)].symbol(), "�");
|
||||
}
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
|
||||
completion.selected = 0;
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.set_cursor_position(caret);
|
||||
panel = draw_completion(frame, &completion, workspace, caret).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(panel.x, panel.y)].symbol(),
|
||||
"O"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(panel.x, panel.y)].bg,
|
||||
Color::Cyan
|
||||
);
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_shows_more_documentation_with_markdown_and_fenced_code_colors() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user