lsp_core/feature/
diagnostics.rs

1use std::{collections::HashMap, fmt::Display, hash::Hash, ops::Range};
2
3use bevy_ecs::{prelude::*, schedule::ScheduleLabel};
4use chumsky::prelude::Simple;
5use futures::channel::mpsc;
6/// [`ScheduleLabel`] related to the PrepareRename schedule
7pub use systems::prefix::undefined_prefix;
8
9use crate::{
10    lsp_types::{Diagnostic, DiagnosticSeverity, TextDocumentItem, Url},
11    prelude::*,
12};
13#[derive(ScheduleLabel, Clone, Eq, PartialEq, Debug, Hash)]
14pub struct Label;
15
16pub fn setup_schedule(world: &mut World) {
17    let mut diagnostics = Schedule::new(Label);
18    diagnostics.add_systems((undefined_prefix,));
19    world.add_schedule(diagnostics);
20}
21
22#[derive(Resource)]
23pub struct DiagnosticPublisher {
24    tx: mpsc::UnboundedSender<DiagnosticItem>,
25    diagnostics: HashMap<crate::lsp_types::Url, Vec<(Diagnostic, &'static str)>>,
26}
27
28impl DiagnosticPublisher {
29    pub fn new() -> (Self, mpsc::UnboundedReceiver<DiagnosticItem>) {
30        let (tx, rx) = mpsc::unbounded();
31        (
32            Self {
33                tx,
34                diagnostics: HashMap::new(),
35            },
36            rx,
37        )
38    }
39
40    pub fn publish(
41        &mut self,
42        params: &TextDocumentItem,
43        diagnostics: Vec<Diagnostic>,
44        reason: &'static str,
45    ) -> Option<()> {
46        let items = self.diagnostics.entry(params.uri.clone()).or_default();
47        items.retain(|(_, r)| *r != reason);
48        items.extend(diagnostics.into_iter().map(|x| (x, reason)));
49        let diagnostics: Vec<_> = items.iter().map(|(x, _)| x).cloned().collect();
50        let uri = params.uri.clone();
51        let version = Some(params.version);
52        let item = DiagnosticItem {
53            diagnostics,
54            uri,
55            version,
56        };
57        self.tx.unbounded_send(item).ok()
58    }
59}
60
61#[derive(Debug)]
62pub struct SimpleDiagnostic {
63    pub range: Range<usize>,
64    pub msg: String,
65    pub severity: Option<DiagnosticSeverity>,
66}
67
68impl SimpleDiagnostic {
69    pub fn new(range: Range<usize>, msg: String) -> Self {
70        Self {
71            range,
72            msg,
73            severity: None,
74        }
75    }
76
77    pub fn new_severity(range: Range<usize>, msg: String, severity: DiagnosticSeverity) -> Self {
78        Self {
79            range,
80            msg,
81            severity: Some(severity),
82        }
83    }
84}
85
86impl<T: Display + Eq + Hash> From<Simple<T>> for SimpleDiagnostic {
87    fn from(e: Simple<T>) -> Self {
88        let msg = if let chumsky::error::SimpleReason::Custom(msg) = e.reason() {
89            msg.clone()
90        } else {
91            format!(
92                "{}{}, expected {}",
93                if e.found().is_some() {
94                    "Unexpected token"
95                } else {
96                    "Unexpected end of input"
97                },
98                if let Some(label) = e.label() {
99                    format!(" while parsing {}", label)
100                } else {
101                    String::new()
102                },
103                if e.expected().len() == 0 {
104                    "something else".to_string()
105                } else {
106                    e.expected()
107                        .map(|expected| match expected {
108                            Some(expected) => format!("'{}'", expected),
109                            None => "end of input".to_string(),
110                        })
111                        .collect::<Vec<_>>()
112                        .join(" or ")
113                },
114            )
115        };
116
117        SimpleDiagnostic::new(e.span(), msg)
118    }
119}
120
121impl<T: Display + Eq + Hash> From<(usize, Simple<T>)> for SimpleDiagnostic {
122    fn from(this: (usize, Simple<T>)) -> Self {
123        let (len, e) = this;
124        let msg = if let chumsky::error::SimpleReason::Custom(msg) = e.reason() {
125            msg.clone()
126        } else {
127            format!(
128                "{}{}, expected {}",
129                if e.found().is_some() {
130                    "Unexpected token"
131                } else {
132                    "Unexpected end of input"
133                },
134                if let Some(label) = e.label() {
135                    format!(" while parsing {}", label)
136                } else {
137                    String::new()
138                },
139                if e.expected().len() == 0 {
140                    "something else".to_string()
141                } else {
142                    e.expected()
143                        .map(|expected| match expected {
144                            Some(expected) => format!("'{}'", expected),
145                            None => "end of input".to_string(),
146                        })
147                        .collect::<Vec<_>>()
148                        .join(" or ")
149                },
150            )
151        };
152
153        let range = (len - e.span().end)..(len - e.span().start);
154        SimpleDiagnostic::new(range, msg)
155    }
156}
157
158#[derive(Clone)]
159pub struct DiagnosticSender {
160    tx: mpsc::UnboundedSender<Vec<SimpleDiagnostic>>,
161}
162
163#[derive(Debug)]
164pub struct DiagnosticItem {
165    pub diagnostics: Vec<Diagnostic>,
166    pub uri: Url,
167    pub version: Option<i32>,
168}
169impl DiagnosticSender {
170    pub fn push(&self, diagnostic: SimpleDiagnostic) -> Option<()> {
171        let out = self.tx.unbounded_send(vec![diagnostic]).ok();
172        out
173    }
174
175    pub fn push_all(&self, diagnostics: Vec<SimpleDiagnostic>) -> Option<()> {
176        self.tx.unbounded_send(diagnostics).ok()
177    }
178}
179
180pub fn publish_diagnostics<L: Lang>(
181    query: Query<
182        (
183            &Errors<L::TokenError>,
184            &Errors<L::ElementError>,
185            &Wrapped<TextDocumentItem>,
186            &RopeC,
187            &crate::components::Label,
188        ),
189        (
190            Or<(
191                Changed<Errors<L::TokenError>>,
192                Changed<Errors<L::ElementError>>,
193            )>,
194            With<Open>,
195        ),
196    >,
197    mut client: ResMut<DiagnosticPublisher>,
198) where
199    L::TokenError: 'static + Clone,
200    L::ElementError: 'static + Clone,
201{
202    for (token_errors, element_errors, params, rope, label) in &query {
203        tracing::info!("Publish diagnostics for {}", label.0);
204        use std::iter::Iterator as _;
205        let token_iter = token_errors
206            .0
207            .iter()
208            .cloned()
209            .map(|x| Into::<SimpleDiagnostic>::into(x));
210        let turtle_iter = element_errors
211            .0
212            .iter()
213            .cloned()
214            .map(|x| Into::<SimpleDiagnostic>::into(x));
215
216        let diagnostics: Vec<_> = Iterator::chain(token_iter, turtle_iter)
217            .flat_map(|item| {
218                let (span, message) = (item.range, item.msg);
219                let start_position = offset_to_position(span.start, &rope.0)?;
220                let end_position = offset_to_position(span.end, &rope.0)?;
221                Some(Diagnostic {
222                    range: crate::lsp_types::Range::new(start_position, end_position),
223                    message,
224                    severity: item.severity,
225                    ..Default::default()
226                })
227            })
228            .collect();
229
230        let _ = client.publish(&params.0, diagnostics, "syntax");
231    }
232}