Skip to main content

swls_core/feature/
diagnostics.rs

1use std::{collections::HashMap, hash::Hash, ops::Range};
2
3use bevy_ecs::{prelude::*, schedule::ScheduleLabel};
4use futures::channel::mpsc;
5
6use crate::{
7    lsp_types::{Diagnostic, DiagnosticSeverity, TextDocumentItem, Url},
8    prelude::*,
9};
10
11#[derive(ScheduleLabel, Clone, Eq, PartialEq, Debug, Hash)]
12pub struct Label;
13
14pub fn setup_schedule(world: &mut World) {
15    let mut diagnostics = Schedule::new(Label);
16    // Prefix diagnostics are disabled pending CST-based reimplementation.
17    // Parse-error diagnostics are still published via publish_diagnostics::<L> added
18    // by each language's setup_world.
19    diagnostics.add_systems(|| {});
20    world.add_schedule(diagnostics);
21}
22
23#[derive(Resource)]
24pub struct DiagnosticPublisher {
25    tx: mpsc::UnboundedSender<DiagnosticItem>,
26    diagnostics: HashMap<crate::lsp_types::Url, Vec<(Diagnostic, &'static str)>>,
27}
28
29impl DiagnosticPublisher {
30    pub fn new() -> (Self, mpsc::UnboundedReceiver<DiagnosticItem>) {
31        let (tx, rx) = mpsc::unbounded();
32        (
33            Self {
34                tx,
35                diagnostics: HashMap::new(),
36            },
37            rx,
38        )
39    }
40
41    pub fn publish(
42        &mut self,
43        params: &TextDocumentItem,
44        diagnostics: Vec<Diagnostic>,
45        reason: &'static str,
46    ) -> Option<()> {
47        let items = self.diagnostics.entry(params.uri.clone()).or_default();
48        items.retain(|(_, r)| *r != reason);
49        items.extend(diagnostics.into_iter().map(|x| (x, reason)));
50        let diagnostics: Vec<_> = items.iter().map(|(x, _)| x).cloned().collect();
51        let uri = params.uri.clone();
52        let version = Some(params.version);
53        let item = DiagnosticItem {
54            diagnostics,
55            uri,
56            version,
57        };
58        self.tx.unbounded_send(item).ok()
59    }
60}
61
62#[derive(Debug)]
63pub struct SimpleDiagnostic {
64    pub range: Range<usize>,
65    pub msg: String,
66    pub severity: Option<DiagnosticSeverity>,
67}
68
69impl SimpleDiagnostic {
70    pub fn new(range: Range<usize>, msg: String) -> Self {
71        Self {
72            range,
73            msg,
74            severity: None,
75        }
76    }
77
78    pub fn new_severity(range: Range<usize>, msg: String, severity: DiagnosticSeverity) -> Self {
79        Self {
80            range,
81            msg,
82            severity: Some(severity),
83        }
84    }
85}
86
87#[derive(Clone)]
88pub struct DiagnosticSender {
89    tx: mpsc::UnboundedSender<Vec<SimpleDiagnostic>>,
90}
91
92#[derive(Debug)]
93pub struct DiagnosticItem {
94    pub diagnostics: Vec<Diagnostic>,
95    pub uri: Url,
96    pub version: Option<i32>,
97}
98
99impl DiagnosticSender {
100    pub fn push(&self, diagnostic: SimpleDiagnostic) -> Option<()> {
101        self.tx.unbounded_send(vec![diagnostic]).ok()
102    }
103
104    pub fn push_all(&self, diagnostics: Vec<SimpleDiagnostic>) -> Option<()> {
105        self.tx.unbounded_send(diagnostics).ok()
106    }
107}
108
109pub fn publish_diagnostics<L: Lang>(
110    query: Query<
111        (
112            &Errors<L::ElementError>,
113            &Wrapped<TextDocumentItem>,
114            &RopeC,
115            &crate::components::Label,
116        ),
117        (Changed<Errors<L::ElementError>>, With<Open>),
118    >,
119    mut client: ResMut<DiagnosticPublisher>,
120) where
121    L::ElementError: 'static + Clone,
122{
123    for (element_errors, params, rope, label) in &query {
124        tracing::debug!("Publish diagnostics for {}", label.0);
125        let diagnostics: Vec<_> = element_errors
126            .0
127            .iter()
128            .cloned()
129            .map(|x| Into::<SimpleDiagnostic>::into(x))
130            .flat_map(|item| {
131                let (span, message) = (item.range, item.msg);
132                let start_position = offset_to_position(span.start, &rope.0)?;
133                let end_position = offset_to_position(span.end, &rope.0)?;
134                Some(Diagnostic {
135                    range: crate::lsp_types::Range::new(start_position, end_position),
136                    message,
137                    severity: item.severity,
138                    ..Default::default()
139                })
140            })
141            .collect();
142
143        let _ = client.publish(&params.0, diagnostics, "syntax");
144    }
145}