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