feat: add asynchronous LSP plugin diagnostics and definitions
This commit is contained in:
+493
-4
@@ -1,9 +1,11 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
fmt::Write as _,
|
||||
io::{self, Stdout},
|
||||
panic::{self, AssertUnwindSafe, PanicHookInfo},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
@@ -29,11 +31,12 @@ use unicode_width::UnicodeWidthStr;
|
||||
use crate::{
|
||||
editor::{Editor, Input, Mode, Palette},
|
||||
files::{DockSide, FilePicker, FilePresentation, FileView},
|
||||
plugin::{Diagnostic, DiagnosticSeverity},
|
||||
session::EditorSession,
|
||||
syntax::{SyntaxCache, SyntaxKind},
|
||||
};
|
||||
|
||||
pub const CAPABILITIES: &[&str] = &["text", "styled_text"];
|
||||
pub const CAPABILITIES: &[&str] = &["text", "styled_text", "diagnostics"];
|
||||
const TAB_WIDTH: usize = 4;
|
||||
|
||||
type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Send + Sync + 'static>;
|
||||
@@ -136,15 +139,28 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
let mut terminal: Terminal<CrosstermBackend<Stdout>> =
|
||||
Terminal::new(CrosstermBackend::new(io::stdout()))?;
|
||||
let mut viewports = BufferViewports::default();
|
||||
let mut redraw = true;
|
||||
while !session.editor().should_quit() {
|
||||
terminal.draw(|frame| viewports.draw(frame, session.editor()))?;
|
||||
redraw |= session.poll();
|
||||
if redraw {
|
||||
terminal.draw(|frame| viewports.draw(frame, session.editor()))?;
|
||||
redraw = false;
|
||||
}
|
||||
if !event::poll(Duration::from_millis(25))? {
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => {
|
||||
if let Some(input) = translate(key) {
|
||||
session.handle(input);
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
Event::Paste(text) => session.handle(Input::Paste(text)),
|
||||
Event::Paste(text) => {
|
||||
session.handle(Input::Paste(text));
|
||||
redraw = true;
|
||||
}
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +236,23 @@ fn visible_line(line: &str, left: usize, width: usize) -> String {
|
||||
visible
|
||||
}
|
||||
|
||||
fn diagnostic_severity(severity: DiagnosticSeverity) -> (usize, Color, &'static str) {
|
||||
match severity {
|
||||
DiagnosticSeverity::Error => (0, Color::Red, "E"),
|
||||
DiagnosticSeverity::Warning => (1, Color::Yellow, "W"),
|
||||
DiagnosticSeverity::Information => (2, Color::Cyan, "I"),
|
||||
DiagnosticSeverity::Hint => (3, Color::DarkGray, "H"),
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_on_row(diagnostic: &Diagnostic, row: usize) -> bool {
|
||||
diagnostic.start.row <= row
|
||||
&& row <= diagnostic.end.row
|
||||
&& (row < diagnostic.end.row
|
||||
|| diagnostic.end.column > 0
|
||||
|| diagnostic.start == diagnostic.end)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
frame: &mut Frame,
|
||||
editor: &Editor,
|
||||
@@ -284,6 +317,15 @@ fn draw(
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
let style = match editor
|
||||
.diagnostics()
|
||||
.filter(|diagnostic| diagnostic_on_row(diagnostic, row))
|
||||
.map(|diagnostic| diagnostic_severity(diagnostic.severity))
|
||||
.min_by_key(|(severity, _, _)| *severity)
|
||||
{
|
||||
Some((_, color, _)) => style.fg(color),
|
||||
None => style,
|
||||
};
|
||||
let number = if row == cursor.row {
|
||||
row + 1
|
||||
} else {
|
||||
@@ -345,6 +387,76 @@ fn draw(
|
||||
column = end;
|
||||
}
|
||||
}
|
||||
for severity in [
|
||||
DiagnosticSeverity::Hint,
|
||||
DiagnosticSeverity::Information,
|
||||
DiagnosticSeverity::Warning,
|
||||
DiagnosticSeverity::Error,
|
||||
] {
|
||||
let style = Style::default()
|
||||
.underline_color(diagnostic_severity(severity).1)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
for diagnostic in editor
|
||||
.diagnostics()
|
||||
.filter(|item| item.severity == severity)
|
||||
{
|
||||
let first = diagnostic.start.row.max(*top);
|
||||
let last = diagnostic.end.row.min(top.saturating_add(height - 1));
|
||||
for row in first..=last {
|
||||
if !diagnostic_on_row(diagnostic, row) {
|
||||
continue;
|
||||
}
|
||||
let start = if row == diagnostic.start.row {
|
||||
diagnostic.start.column
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end = if row == diagnostic.end.row {
|
||||
diagnostic
|
||||
.end
|
||||
.column
|
||||
.saturating_add(usize::from(diagnostic.start == diagnostic.end))
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
let line = &editor.lines()[row];
|
||||
let y = viewport.y + (row - *top) as u16;
|
||||
let right = left.saturating_add(width);
|
||||
let mut column = 0;
|
||||
let mut grapheme_count = 0;
|
||||
for (index, grapheme) in line.graphemes(true).enumerate() {
|
||||
let cells = if grapheme == "\t" {
|
||||
TAB_WIDTH - column % TAB_WIDTH
|
||||
} else {
|
||||
UnicodeWidthStr::width(grapheme)
|
||||
};
|
||||
let next = column + cells;
|
||||
if index >= start
|
||||
&& index < end
|
||||
&& (grapheme == "\t" || (column >= *left && next <= right))
|
||||
{
|
||||
for cell in column.max(*left)..next.min(right) {
|
||||
frame.buffer_mut()[(viewport.x + (cell - *left) as u16, y)]
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
column = next;
|
||||
grapheme_count = index + 1;
|
||||
if column >= right {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if start <= grapheme_count
|
||||
&& end > grapheme_count
|
||||
&& column >= *left
|
||||
&& column < right
|
||||
{
|
||||
frame.buffer_mut()[(viewport.x + (column - *left) as u16, y)]
|
||||
.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(found) = editor.search_match()
|
||||
&& found.start.row >= *top
|
||||
&& found.start.row < top.saturating_add(height)
|
||||
@@ -419,6 +531,15 @@ fn draw(
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mut diagnostic_counts = [0; 4];
|
||||
for diagnostic in editor.diagnostics() {
|
||||
diagnostic_counts[diagnostic_severity(diagnostic.severity).0] += 1;
|
||||
}
|
||||
for (label, count) in ["E", "W", "I", "H"].into_iter().zip(diagnostic_counts) {
|
||||
if count > 0 {
|
||||
write!(status, " {label}:{count}").unwrap();
|
||||
}
|
||||
}
|
||||
write!(
|
||||
status,
|
||||
" {}{} {}:{}",
|
||||
@@ -462,7 +583,18 @@ fn draw(
|
||||
prompt_area.y,
|
||||
));
|
||||
} else {
|
||||
frame.render_widget(Paragraph::new(editor.message()), prompt_area);
|
||||
let message = if let Some(diagnostic) = editor.diagnostic_at_cursor() {
|
||||
let (_, _, label) = diagnostic_severity(diagnostic.severity);
|
||||
let diagnostic = display_text(&diagnostic.message);
|
||||
Cow::Owned(if editor.message().is_empty() {
|
||||
format!("{label}: {diagnostic}")
|
||||
} else {
|
||||
format!("{} | {label}: {diagnostic}", editor.message())
|
||||
})
|
||||
} else {
|
||||
Cow::Borrowed(editor.message())
|
||||
};
|
||||
frame.render_widget(Paragraph::new(message), prompt_area);
|
||||
if height > 0 && width > 0 {
|
||||
let column = display_column(&editor.lines()[cursor.row], cursor.column);
|
||||
frame.set_cursor_position((
|
||||
@@ -845,6 +977,339 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn annotation(
|
||||
start: (usize, usize),
|
||||
end: (usize, usize),
|
||||
severity: DiagnosticSeverity,
|
||||
message: &str,
|
||||
) -> Diagnostic {
|
||||
Diagnostic {
|
||||
start: crate::plugin::TextPosition {
|
||||
row: start.0,
|
||||
column: start.1,
|
||||
},
|
||||
end: crate::plugin::TextPosition {
|
||||
row: end.0,
|
||||
column: end.1,
|
||||
},
|
||||
severity,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_underlines_preserve_syntax_and_whole_graphemes_at_viewport_edges() {
|
||||
let directory = Directory::new();
|
||||
let source = "#a\t界e\u{301}👩🚀z";
|
||||
let mut editor = Editor::open(Some(directory.file("unicode.py", source)), vec![]).unwrap();
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
vec![
|
||||
annotation((0, 2), (0, 6), DiagnosticSeverity::Warning, "range"),
|
||||
annotation((0, 4), (0, 5), DiagnosticSeverity::Error, "combining"),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let mut syntax = SyntaxCache::new();
|
||||
for (offset, width, caret, underlines) in [
|
||||
(
|
||||
2,
|
||||
5,
|
||||
4,
|
||||
vec![
|
||||
Some(Color::Yellow),
|
||||
Some(Color::Yellow),
|
||||
Some(Color::Yellow),
|
||||
Some(Color::Yellow),
|
||||
Some(Color::Red),
|
||||
],
|
||||
),
|
||||
(5, 2, 4, vec![None, Some(Color::Red)]),
|
||||
(7, 2, 5, vec![Some(Color::Yellow), Some(Color::Yellow)]),
|
||||
(8, 2, 6, vec![None, None]),
|
||||
(
|
||||
0,
|
||||
5,
|
||||
2,
|
||||
vec![None, None, Some(Color::Yellow), Some(Color::Yellow), None],
|
||||
),
|
||||
] {
|
||||
editor.handle(Input::Home);
|
||||
for _ in 0..caret {
|
||||
editor.handle(Input::Right);
|
||||
}
|
||||
let area = Rect::new(3, 2, width + 2, 4);
|
||||
let mut terminal = Terminal::with_options(
|
||||
ratatui::backend::TestBackend::new(20, 8),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Fixed(area),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let (mut top, mut left) = (0, offset);
|
||||
let rendered = terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left, &mut syntax))
|
||||
.unwrap()
|
||||
.buffer
|
||||
.clone();
|
||||
assert_eq!(left, offset);
|
||||
for (cell, underline) in underlines.into_iter().enumerate() {
|
||||
let position = (area.x + 2 + cell as u16, area.y);
|
||||
assert_eq!(
|
||||
rendered[position].modifier.contains(Modifier::UNDERLINED),
|
||||
underline.is_some(),
|
||||
"left={offset}, cell={cell}",
|
||||
);
|
||||
if let Some(color) = underline {
|
||||
assert_eq!(rendered[position].underline_color, color);
|
||||
assert_eq!(rendered[position].fg, Color::DarkGray);
|
||||
}
|
||||
assert_eq!(rendered[position].bg, Color::Reset);
|
||||
}
|
||||
assert_eq!(rendered[(area.x, area.y)].fg, Color::Red);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(0, 0)].modifier,
|
||||
Modifier::empty()
|
||||
);
|
||||
assert_eq!(editor.lines(), &[source]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_render_newlines_anchors_counts_and_messages_without_hiding_core_errors() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"abc\n\nxy\nlast".into(),
|
||||
)])
|
||||
.unwrap();
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
vec![
|
||||
annotation((0, 2), (0, 2), DiagnosticSeverity::Error, "point"),
|
||||
annotation((0, 1), (3, 0), DiagnosticSeverity::Warning, "multiline"),
|
||||
annotation((3, 4), (3, 4), DiagnosticSeverity::Information, "end"),
|
||||
annotation((3, 0), (3, 4), DiagnosticSeverity::Hint, "hint"),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
editor.handle(Input::Home);
|
||||
editor.handle(Input::Right);
|
||||
editor.handle(Input::Right);
|
||||
editor.set_message("write failed: permission denied");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 7)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| viewports.draw(frame, &editor))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (position, color) in [
|
||||
((3, 0), Color::Yellow),
|
||||
((4, 0), Color::Red),
|
||||
((5, 0), Color::Yellow),
|
||||
((2, 1), Color::Yellow),
|
||||
((2, 2), Color::Yellow),
|
||||
((3, 2), Color::Yellow),
|
||||
((4, 2), Color::Yellow),
|
||||
((2, 3), Color::DarkGray),
|
||||
((6, 3), Color::Cyan),
|
||||
] {
|
||||
assert!(buffer[position].modifier.contains(Modifier::UNDERLINED));
|
||||
assert_eq!(buffer[position].underline_color, color);
|
||||
}
|
||||
for (row, color) in [
|
||||
(0, Color::Red),
|
||||
(1, Color::Yellow),
|
||||
(2, Color::Yellow),
|
||||
(3, Color::Cyan),
|
||||
] {
|
||||
assert_eq!(buffer[(0, row)].fg, color);
|
||||
}
|
||||
assert!(!buffer[(6, 0)].modifier.contains(Modifier::UNDERLINED));
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("E:1 W:1 I:1 H:1"), "{text}");
|
||||
assert!(
|
||||
text.contains("write failed: permission denied | E: point"),
|
||||
"{text}"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(4, 0),
|
||||
);
|
||||
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Char('!'));
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
terminal
|
||||
.draw(|frame| viewports.draw(frame, &editor))
|
||||
.unwrap();
|
||||
assert_eq!(editor.diagnostics().count(), 0);
|
||||
assert!(!screen_text(&terminal).contains("E:1"));
|
||||
assert!(
|
||||
!terminal.backend().buffer()[(4, 0)]
|
||||
.modifier
|
||||
.contains(Modifier::UNDERLINED)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_session_updates_redraw_annotations_and_navigation_while_native_editing_continues() {
|
||||
let directory = Directory::new();
|
||||
let source = directory.file("source.txt", "broken TARGET");
|
||||
let target_text = format!("{}{}TARGET", "# line\n".repeat(15), "#".repeat(40));
|
||||
let target = directory.file("target.py", &target_text);
|
||||
directory.file(
|
||||
"semantic.rb",
|
||||
r#"
|
||||
Vamp.plugin "semantic", requires: ["text", "diagnostics"], async: true
|
||||
work = Queue.new
|
||||
Vamp.on_document { |event| work << ["document", event] }
|
||||
Vamp.command "fixture_definition" do |editor, _|
|
||||
raise "No target reference" unless editor.text.include?("TARGET")
|
||||
work << ["definition", editor.buffer_id, editor.revision, editor.request_id]
|
||||
end
|
||||
Thread.new do
|
||||
documents = {}
|
||||
loop do
|
||||
kind, *args = work.pop
|
||||
if kind == "document"
|
||||
event = args[0]
|
||||
document = event.fetch("document")
|
||||
if event.fetch("event") == "close"
|
||||
documents.delete(document.fetch("id"))
|
||||
next
|
||||
end
|
||||
documents[document.fetch("id")] = document
|
||||
next unless ["open", "change"].include?(event.fetch("event"))
|
||||
items = []
|
||||
document.fetch("text").split("\n", -1).each_with_index do |line, row|
|
||||
graphemes = line.each_grapheme_cluster.to_a
|
||||
graphemes.each_cons(6).with_index do |token, column|
|
||||
next unless token.join == "broken"
|
||||
items << {start: {row: row, column: column},
|
||||
end: {row: row, column: column + 6},
|
||||
severity: "error", message: "Unresolved token"}
|
||||
end
|
||||
end
|
||||
Vamp.publish([{type: "diagnostics", buffer_id: document.fetch("id"),
|
||||
revision: document.fetch("revision"), items: items}])
|
||||
else
|
||||
target = documents.values.find { |document| document["path"] == Vamp.options.fetch("target") }
|
||||
raise "Target document is not open" unless target
|
||||
lines = target.fetch("text").split("\n", -1)
|
||||
row = lines.index { |line| line.include?("TARGET") }
|
||||
raise "Target definition missing" unless row
|
||||
column = lines[row].each_grapheme_cluster.to_a.each_cons(6).find_index { |token| token.join == "TARGET" }
|
||||
Vamp.publish([{type: "navigate", buffer_id: args[0], revision: args[1],
|
||||
path: target.fetch("path"), row: row, column: column}],
|
||||
request_id: args[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
"#,
|
||||
);
|
||||
let config = directory.file(
|
||||
"config.rb",
|
||||
&format!(
|
||||
"Vamp.configure do |config|\n config.clipboard_mode = \"separate\"\n config.plugin \"semantic.rb\", options: {{target: {}}}\nend\n",
|
||||
serde_json::to_string(&target.to_string_lossy()).unwrap(),
|
||||
),
|
||||
);
|
||||
let mut session = EditorSession::open(
|
||||
Some(source),
|
||||
CAPABILITIES
|
||||
.iter()
|
||||
.map(|capability| (*capability).into())
|
||||
.collect(),
|
||||
Some(&config),
|
||||
)
|
||||
.unwrap();
|
||||
let source_id = session.editor().buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(18, 6)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| viewports.draw(frame, session.editor()))
|
||||
.unwrap();
|
||||
|
||||
fn poll_until(
|
||||
session: &mut EditorSession,
|
||||
terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
viewports: &mut BufferViewports,
|
||||
ready: impl Fn(&Editor) -> bool,
|
||||
) {
|
||||
for _ in 0..2_000 {
|
||||
if session.poll() {
|
||||
terminal
|
||||
.draw(|frame| viewports.draw(frame, session.editor()))
|
||||
.unwrap();
|
||||
}
|
||||
if ready(session.editor()) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
panic!(
|
||||
"async fixture did not update editor: {}",
|
||||
session.editor().message()
|
||||
);
|
||||
}
|
||||
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Char('!'));
|
||||
assert_eq!(session.editor().mode(), Mode::Insert);
|
||||
assert_eq!(session.editor().lines(), &["broken TARGET!"]);
|
||||
poll_until(&mut session, &mut terminal, &mut viewports, |editor| {
|
||||
editor.diagnostics().count() == 1
|
||||
});
|
||||
assert!(screen_text(&terminal).contains("E:1"));
|
||||
assert!(
|
||||
terminal.backend().buffer()[(2, 0)]
|
||||
.modifier
|
||||
.contains(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(session.editor().mode(), Mode::Insert);
|
||||
session.handle(Input::Escape);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &["broken TARGET"]);
|
||||
assert_eq!(session.editor().diagnostics().count(), 0);
|
||||
|
||||
session.execute(&format!("edit {}", target.display()));
|
||||
let target_id = session.editor().buffer_id();
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Char('!'));
|
||||
session.handle(Input::Escape);
|
||||
session.execute(&format!("buffer {source_id}"));
|
||||
session.execute("fixture_definition");
|
||||
session.handle(Input::Char('l'));
|
||||
assert_eq!(session.editor().buffer_id(), source_id);
|
||||
poll_until(&mut session, &mut terminal, &mut viewports, |editor| {
|
||||
editor.buffer_id() == target_id
|
||||
});
|
||||
assert_eq!(session.editor().buffer_count(), 2);
|
||||
assert!(session.editor().dirty());
|
||||
assert_eq!(session.editor().lines()[0], "!# line");
|
||||
assert_eq!(session.editor().cursor().row, 15);
|
||||
assert_eq!(session.editor().cursor().column, 40);
|
||||
assert_eq!(session.editor().mode(), Mode::Normal);
|
||||
assert_eq!(viewports.offsets[&target_id], (12, 26));
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
|
||||
"T"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(cursor.x, cursor.y)].fg,
|
||||
Color::DarkGray
|
||||
);
|
||||
assert_eq!(fs::read_to_string(target).unwrap(), target_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_tokens_refresh_after_native_edits_and_undo_without_changing_text_on_draw() {
|
||||
let directory = Directory::new();
|
||||
@@ -1029,6 +1494,19 @@ mod tests {
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
vec![annotation(
|
||||
(0, 2),
|
||||
(2, 6),
|
||||
DiagnosticSeverity::Error,
|
||||
"overlays",
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('/'));
|
||||
for ch in "word".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
@@ -1044,6 +1522,11 @@ mod tests {
|
||||
for x in 4..8 {
|
||||
assert_eq!(terminal.backend().buffer()[(x, y)].fg, Color::Black);
|
||||
assert_eq!(terminal.backend().buffer()[(x, y)].bg, Color::Yellow);
|
||||
assert!(
|
||||
terminal.backend().buffer()[(x, y)]
|
||||
.modifier
|
||||
.contains(Modifier::UNDERLINED)
|
||||
);
|
||||
}
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Char('e'));
|
||||
@@ -1066,6 +1549,7 @@ mod tests {
|
||||
assert_eq!(buffer[(x, y)].fg, Color::White);
|
||||
assert_eq!(buffer[(x, y)].bg, Color::Blue);
|
||||
}
|
||||
assert!(buffer[(x, y)].modifier.contains(Modifier::UNDERLINED));
|
||||
}
|
||||
assert_eq!(buffer[(2, y)].fg, Color::DarkGray);
|
||||
}
|
||||
@@ -1077,6 +1561,11 @@ mod tests {
|
||||
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
|
||||
assert_eq!(buffer[(x, caret.row as u16)].fg, Color::Black);
|
||||
assert_eq!(buffer[(x, caret.row as u16)].bg, Color::Magenta);
|
||||
assert!(
|
||||
buffer[(x, caret.row as u16)]
|
||||
.modifier
|
||||
.contains(Modifier::UNDERLINED)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user