fix: defer diagnostic display until leaving Insert mode

This commit is contained in:
2026-10-10 22:34:38 +02:00
parent 3f05287d79
commit f6da7922a1
5 changed files with 67 additions and 7 deletions
+2
View File
@@ -210,6 +210,8 @@ Interactive LSP commands use dotted names (`:lsp.hover`, `:lsp.definition`, `:ls
Diagnostics appear as severity-colored gutter numbers and text underlines; the primary caret's diagnostic appears in the message row. `:lsp.diagnostics` reports the current document's server state. Older versioned diagnostics are discarded; unversioned diagnostics can only be associated with the arrival-time document revision. Editing clears previous annotations immediately. Server startup/transport failures are reported and clear that server's diagnostics; other servers and native editing remain available. There are no automatic restarts. Diagnostics appear as severity-colored gutter numbers and text underlines; the primary caret's diagnostic appears in the message row. `:lsp.diagnostics` reports the current document's server state. Older versioned diagnostics are discarded; unversioned diagnostics can only be associated with the arrival-time document revision. Editing clears previous annotations immediately. Server startup/transport failures are reported and clear that server's diagnostics; other servers and native editing remain available. There are no automatic restarts.
Insert mode hides all diagnostic gutter colors, underlines, counts, and caret messages. Incoming results are still stored; leaving Insert reveals only the latest results matching the current text revision. Editing invalidates earlier results, so stale annotations never reappear on exit. Document synchronization stays live for completion and semantic highlighting; this display policy does not stop server-side diagnostic analysis.
`:lsp.hover` or a configured hover action requests information at the primary caret once, regardless of count or selection count. The response opens a read-only floating popup: arrows/Ctrl+h/j/k/l scroll vertically/horizontally, Home/End select the first/last logical line, and Escape dismisses without collapsing carets. Other input dismisses and continues normal editing. Changes to the source buffer/revision/caret, intervening input, or newer requests prevent pending hover replies from reopening it. `:lsp.hover` or a configured hover action requests information at the primary caret once, regardless of count or selection count. The response opens a read-only floating popup: arrows/Ctrl+h/j/k/l scroll vertically/horizontally, Home/End select the first/last logical line, and Escape dismisses without collapsing carets. Other input dismisses and continues normal editing. Changes to the source buffer/revision/caret, intervening input, or newer requests prevent pending hover replies from reopening it.
Hover accepts plaintext/Markdown MarkupContent and string/code MarkedString forms, including arrays. The popup grows up to 100 columns and 24 rows, clamped to the available document workspace and content height. Multiline content and Markdown markers remain visible, with the same Markdown syntax colors as document buffers; named fenced code blocks use their language's bundled highlighter. Highlighting is cached across scrolling and redraws. Horizontal scrolling exposes long lines without splitting graphemes. Missing/unsupported/invalid hover results report a message without changing text, selections, or undo history. Content is bounded to 4 MiB and terminal controls are sanitized. Hover accepts plaintext/Markdown MarkupContent and string/code MarkedString forms, including arrays. The popup grows up to 100 columns and 24 rows, clamped to the available document workspace and content height. Multiline content and Markdown markers remain visible, with the same Markdown syntax colors as document buffers; named fenced code blocks use their language's bundled highlighter. Highlighting is cached across scrolling and redraws. Horizontal scrolling exposes long lines without splitting graphemes. Missing/unsupported/invalid hover results report a message without changing text, selections, or undo history. Content is bounded to 4 MiB and terminal controls are sanitized.
+4 -1
View File
@@ -1335,7 +1335,10 @@ impl Editor {
self.active self.active
.diagnostics .diagnostics
.values() .values()
.filter(|annotations| annotations.revision == self.active.text_revision) .filter(|annotations| {
self.active.mode != Mode::Insert
&& annotations.revision == self.active.text_revision
})
.flat_map(|annotations| &annotations.items) .flat_map(|annotations| &annotations.items)
} }
+1 -1
View File
@@ -2805,7 +2805,7 @@ end
.map(|diagnostic| diagnostic.message.as_str()), .map(|diagnostic| diagnostic.message.as_str()),
None None
); );
assert_eq!(session.editor().diagnostics().count(), 1); assert_eq!(session.editor().diagnostics().count(), 0);
session.handle(Input::Char('y')); session.handle(Input::Char('y'));
assert_eq!(session.editor().diagnostics().count(), 0); assert_eq!(session.editor().diagnostics().count(), 0);
session.handle(Input::Escape); session.handle(Input::Escape);
+58 -4
View File
@@ -2098,6 +2098,47 @@ mod tests {
ratatui::layout::Position::new(4, 0), ratatui::layout::Position::new(4, 0),
); );
editor.handle(Input::Char('i'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(editor.diagnostics().count(), 0);
assert!(!screen_text(&terminal).contains("E:1"));
assert!(!screen_text(&terminal).contains("E: point"));
assert_ne!(terminal.backend().buffer()[(0, 0)].fg, Color::Red);
assert!(
!terminal.backend().buffer()[(4, 0)]
.modifier
.contains(Modifier::UNDERLINED)
);
for message in ["intermediate", "latest"] {
editor
.replace_diagnostics(
"fixture",
editor.buffer_id(),
editor.text_revision(),
vec![annotation(
(0, 0),
(0, 3),
DiagnosticSeverity::Error,
message,
)],
)
.unwrap();
}
editor.handle(Input::Escape);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(screen_text(&terminal).contains("E:1"));
assert!(screen_text(&terminal).contains("E: latest"));
assert!(!screen_text(&terminal).contains("intermediate"));
assert_eq!(terminal.backend().buffer()[(0, 0)].fg, Color::Red);
assert!(
terminal.backend().buffer()[(3, 0)]
.modifier
.contains(Modifier::UNDERLINED)
);
editor.handle(Input::Char('i')); editor.handle(Input::Char('i'));
editor.handle(Input::Char('!')); editor.handle(Input::Char('!'));
assert_eq!(editor.mode(), Mode::Insert); assert_eq!(editor.mode(), Mode::Insert);
@@ -2111,6 +2152,8 @@ mod tests {
.modifier .modifier
.contains(Modifier::UNDERLINED) .contains(Modifier::UNDERLINED)
); );
editor.handle(Input::Escape);
assert_eq!(editor.diagnostics().count(), 0);
} }
#[test] #[test]
@@ -2153,7 +2196,8 @@ Thread.new do
end end
end end
Vamp.publish([{type: "diagnostics", buffer_id: document.fetch("id"), Vamp.publish([{type: "diagnostics", buffer_id: document.fetch("id"),
revision: document.fetch("revision"), items: items}]) revision: document.fetch("revision"), items: items},
{type: "message", text: "diagnostic revision #{document.fetch('revision')}"}])
else else
target = documents.values.find { |document| document["path"] == Vamp.options.fetch("target") } target = documents.values.find { |document| document["path"] == Vamp.options.fetch("target") }
raise "Target document is not open" unless target raise "Target document is not open" unless target
@@ -2220,16 +2264,26 @@ end
assert_eq!(session.editor().mode(), Mode::Insert); assert_eq!(session.editor().mode(), Mode::Insert);
assert_eq!(session.editor().lines(), &["broken TARGET!"]); assert_eq!(session.editor().lines(), &["broken TARGET!"]);
poll_until(&mut session, &mut terminal, &mut viewports, |editor| { poll_until(&mut session, &mut terminal, &mut viewports, |editor| {
editor.diagnostics().count() == 1 editor.message() == "diagnostic revision 1"
}); });
assert_eq!(session.editor().diagnostics().count(), 0);
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);
terminal
.draw(|frame| viewports.draw(frame, session.editor()))
.unwrap();
assert!(screen_text(&terminal).contains("E:1")); assert!(screen_text(&terminal).contains("E:1"));
assert!( assert!(
terminal.backend().buffer()[(2, 0)] terminal.backend().buffer()[(2, 0)]
.modifier .modifier
.contains(Modifier::UNDERLINED) .contains(Modifier::UNDERLINED)
); );
assert_eq!(session.editor().mode(), Mode::Insert);
session.handle(Input::Escape);
session.handle(Input::Char('u')); session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), &["broken TARGET"]); assert_eq!(session.editor().lines(), &["broken TARGET"]);
assert_eq!(session.editor().diagnostics().count(), 0); assert_eq!(session.editor().diagnostics().count(), 0);
+2 -1
View File
@@ -2576,8 +2576,9 @@ fn lsp_completion_feature_errors_keep_healthy_services() {
completion_reply(&mut session, &fixture, index as u64 + 1, response); completion_reply(&mut session, &fixture, index as u64 + 1, response);
assert!(session.editor().completion().is_none()); assert!(session.editor().completion().is_none());
assert_eq!(session.editor().context().text, "A😀e\u{301}Z\n"); assert_eq!(session.editor().context().text, "A😀e\u{301}Z\n");
assert_eq!(session.editor().diagnostics().count(), 2); assert_eq!(session.editor().diagnostics().count(), 0);
session.handle(Input::Escape); session.handle(Input::Escape);
assert_eq!(session.editor().diagnostics().count(), 2);
} }
session.execute("lsp.hover"); session.execute("lsp.hover");
until(&mut session, &fixture, |session| { until(&mut session, &fixture, |session| {