use crate::model::buffer::Buffer;
use crate::state::EditorState;
use crate::view::overlay::{Overlay, OverlayFace, OverlayNamespace};
use lsp_types::{Diagnostic, DiagnosticSeverity};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::sync::{LazyLock, Mutex};
pub fn lsp_diagnostic_namespace() -> OverlayNamespace {
OverlayNamespace::from_string("lsp-diagnostic".to_string())
}
static DIAGNOSTIC_CACHE: LazyLock<Mutex<HashMap<String, u64>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn compute_diagnostic_hash(diagnostics: &[Diagnostic]) -> u64 {
let mut hasher = DefaultHasher::new();
diagnostics.len().hash(&mut hasher);
for diag in diagnostics {
diag.range.start.line.hash(&mut hasher);
diag.range.start.character.hash(&mut hasher);
diag.range.end.line.hash(&mut hasher);
diag.range.end.character.hash(&mut hasher);
let severity_value: i32 = match diag.severity {
Some(DiagnosticSeverity::ERROR) => 1,
Some(DiagnosticSeverity::WARNING) => 2,
Some(DiagnosticSeverity::INFORMATION) => 3,
Some(DiagnosticSeverity::HINT) => 4,
None => 0,
_ => -1,
};
severity_value.hash(&mut hasher);
diag.message.hash(&mut hasher);
if let Some(source) = &diag.source {
source.hash(&mut hasher);
}
}
hasher.finish()
}
pub fn apply_diagnostics_to_state_cached(
state: &mut EditorState,
diagnostics: &[Diagnostic],
theme: &crate::view::theme::Theme,
) {
let cache_key = match state.buffer.file_path() {
Some(path) => path.to_string_lossy().to_string(),
None => return apply_diagnostics_to_state(state, diagnostics, theme),
};
let new_hash = compute_diagnostic_hash(diagnostics);
if let Ok(cache) = DIAGNOSTIC_CACHE.lock() {
if let Some(&cached_hash) = cache.get(&cache_key) {
if cached_hash == new_hash {
return;
}
}
}
apply_diagnostics_to_state(state, diagnostics, theme);
if let Ok(mut cache) = DIAGNOSTIC_CACHE.lock() {
cache.insert(cache_key, new_hash);
}
}
pub fn diagnostic_to_overlay(
diagnostic: &Diagnostic,
buffer: &Buffer,
theme: &crate::view::theme::Theme,
) -> Option<(Range<usize>, OverlayFace, i32)> {
let start_line = diagnostic.range.start.line as usize;
let start_char = diagnostic.range.start.character as usize;
let end_line = diagnostic.range.end.line as usize;
let end_char = diagnostic.range.end.character as usize;
let start_byte = buffer.lsp_position_to_byte(start_line, start_char);
let end_byte = buffer.lsp_position_to_byte(end_line, end_char);
let (face, priority) = match diagnostic.severity {
Some(DiagnosticSeverity::ERROR) => (
OverlayFace::Background {
color: theme.diagnostic_error_bg,
},
100, ),
Some(DiagnosticSeverity::WARNING) => (
OverlayFace::Background {
color: theme.diagnostic_warning_bg,
},
50, ),
Some(DiagnosticSeverity::INFORMATION) => (
OverlayFace::Background {
color: theme.diagnostic_info_bg,
},
30, ),
Some(DiagnosticSeverity::HINT) | None => (
OverlayFace::Background {
color: theme.diagnostic_hint_bg,
},
10, ),
_ => return None, };
Some((start_byte..end_byte, face, priority))
}
pub fn apply_diagnostics_to_state(
state: &mut EditorState,
diagnostics: &[Diagnostic],
theme: &crate::view::theme::Theme,
) {
let ns = lsp_diagnostic_namespace();
state.overlays.clear_namespace(&ns, &mut state.marker_list);
let mut added_count = 0;
for diagnostic in diagnostics {
if let Some((range, face, priority)) =
diagnostic_to_overlay(diagnostic, &state.buffer, theme)
{
let message = diagnostic.message.clone();
let overlay = Overlay::with_namespace(&mut state.marker_list, range, face, ns.clone())
.with_priority_value(priority)
.with_message(message);
state.overlays.add(overlay);
added_count += 1;
}
}
if added_count > 0 {
tracing::debug!("Applied {} diagnostic overlays", added_count);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::buffer::Buffer;
use crate::view::theme;
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
#[test]
fn test_lsp_position_to_byte() {
let buffer = Buffer::from_str_test("hello\nworld\ntest");
assert_eq!(buffer.lsp_position_to_byte(0, 0), 0);
assert_eq!(buffer.lsp_position_to_byte(0, 5), 5);
assert_eq!(buffer.lsp_position_to_byte(1, 0), 6);
assert_eq!(buffer.lsp_position_to_byte(1, 5), 11);
assert_eq!(buffer.lsp_position_to_byte(2, 0), 12);
assert_eq!(buffer.lsp_position_to_byte(10, 0), buffer.len());
}
#[test]
fn test_diagnostic_to_overlay_error() {
let buffer = Buffer::from_str_test("hello world");
let diagnostic = Diagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: None,
message: "Test error".to_string(),
related_information: None,
tags: None,
data: None,
};
let theme = crate::view::theme::Theme::load_builtin(theme::THEME_DARK).unwrap();
let result = diagnostic_to_overlay(&diagnostic, &buffer, &theme);
assert!(result.is_some());
let (range, face, priority) = result.unwrap();
assert_eq!(range, 0..5);
assert_eq!(priority, 100);
match face {
OverlayFace::Background { color } => {
assert_eq!(color, theme.diagnostic_error_bg);
}
_ => panic!("Expected Background face"),
}
}
#[test]
fn test_diagnostic_to_overlay_warning() {
let buffer = Buffer::from_str_test("hello world");
let diagnostic = Diagnostic {
range: Range {
start: Position {
line: 0,
character: 6,
},
end: Position {
line: 0,
character: 11,
},
},
severity: Some(DiagnosticSeverity::WARNING),
code: None,
code_description: None,
source: None,
message: "Test warning".to_string(),
related_information: None,
tags: None,
data: None,
};
let theme = crate::view::theme::Theme::load_builtin(theme::THEME_DARK).unwrap();
let result = diagnostic_to_overlay(&diagnostic, &buffer, &theme);
assert!(result.is_some());
let (range, face, priority) = result.unwrap();
assert_eq!(range, 6..11);
assert_eq!(priority, 50);
match face {
OverlayFace::Background { color } => {
assert_eq!(color, theme.diagnostic_warning_bg);
}
_ => panic!("Expected Background face"),
}
}
#[test]
fn test_diagnostic_to_overlay_multiline() {
let buffer = Buffer::from_str_test("line1\nline2\nline3");
let diagnostic = Diagnostic {
range: Range {
start: Position {
line: 0,
character: 3,
},
end: Position {
line: 1,
character: 2,
},
},
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: None,
message: "Multi-line error".to_string(),
related_information: None,
tags: None,
data: None,
};
let theme = crate::view::theme::Theme::load_builtin(theme::THEME_DARK).unwrap();
let result = diagnostic_to_overlay(&diagnostic, &buffer, &theme);
assert!(result.is_some());
let (range, _, _) = result.unwrap();
assert_eq!(range.start, 3);
assert_eq!(range.end, 8);
}
}