feat: expand completion snippets and navigate placeholders

This commit is contained in:
2026-10-10 19:12:13 +02:00
parent b6059d5ed9
commit 232be6a032
12 changed files with 2194 additions and 49 deletions
+81 -1
View File
@@ -340,6 +340,7 @@ fn translate(key: KeyEvent) -> Option<Input> {
KeyCode::Home => Input::Home,
KeyCode::End => Input::End,
KeyCode::Tab => Input::Char('\t'),
KeyCode::BackTab => Input::BackTab,
_ => return None,
})
}
@@ -683,7 +684,11 @@ fn draw(
}
}
}
for selection in selections.iter().filter(|_| !editor.visual_linewise()) {
for selection in selections
.iter()
.filter(|_| !editor.visual_linewise())
.chain(editor.snippet_selections())
{
let first = selection.start.row.max(*top);
let last = selection.end.row.min(top.saturating_add(height - 1));
let style = Style::default().fg(Color::White).bg(Color::Blue);
@@ -1583,6 +1588,8 @@ fn semantic_style(token: &SemanticToken) -> Style {
}
#[cfg(test)]
// These fixtures intentionally contain ranges, not lists of indices.
#[allow(clippy::single_range_in_vec_init)]
mod tests {
use super::*;
use std::{
@@ -1643,6 +1650,7 @@ mod tests {
detail: Some("\tTAIL".into()),
primary_edit: 0,
edits: vec![],
snippet: None,
})
.collect(),
selected: 20,
@@ -1707,6 +1715,78 @@ mod tests {
}
}
#[test]
fn snippet_highlight_tracks_argument_graphemes_without_filling_the_row() {
use crate::plugin::{
CompletionEdit, CompletionItem, CompletionSnippet, SnippetTabstop, TextPosition,
};
let mut editor = Editor::open(None, vec![]).unwrap();
editor.handle(Input::Char('i'));
editor
.show_completion(vec![CompletionItem {
label: "call".into(),
detail: None,
primary_edit: 0,
edits: vec![CompletionEdit {
start: TextPosition { row: 0, column: 0 },
end: TextPosition { row: 0, column: 0 },
text: "call(界e\u{301}, tail)".into(),
}],
snippet: Some(CompletionSnippet {
tabstops: vec![
SnippetTabstop {
index: 1,
ranges: vec![5..11],
ancestors: vec![vec![]],
},
SnippetTabstop {
index: 2,
ranges: vec![13..17],
ancestors: vec![vec![]],
},
SnippetTabstop {
index: 0,
ranges: vec![18..18],
ancestors: vec![vec![]],
},
],
}),
}])
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(28, 6)).unwrap();
let mut viewports = BufferViewports::default();
for (key, selected, caret) in [
(None, 7..10, Position::new(7, 0)),
(Some(Input::Char('\t')), 12..16, Position::new(12, 0)),
] {
if let Some(key) = key {
editor.handle(key);
}
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let buffer = terminal.backend().buffer();
// A wide glyph's leading cell owns its terminal style.
for x in (0..28).filter(|x| *x != 8) {
assert_eq!(
buffer[(x, 0)].bg == Color::Blue,
selected.contains(&x),
"column {x}, selection {:?}, screen:\n{}",
editor.snippet_selections(),
screen_text(&terminal),
);
}
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
}
editor.handle(Input::Escape);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
for x in 0..28 {
assert_ne!(terminal.backend().buffer()[(x, 0)].bg, Color::Blue);
}
}
#[test]
fn hover_shows_more_documentation_with_markdown_and_fenced_code_colors() {
let mut editor = Editor::open(None, vec![]).unwrap();