kcl_lib/
errors.rs

1use indexmap::IndexMap;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
6
7#[cfg(feature = "artifact-graph")]
8use crate::execution::{ArtifactCommand, ArtifactGraph, Operation};
9use crate::{
10    execution::DefaultPlanes,
11    lsp::IntoDiagnostic,
12    modules::{ModulePath, ModuleSource},
13    source_range::SourceRange,
14    ModuleId,
15};
16
17/// How did the KCL execution fail
18#[derive(thiserror::Error, Debug)]
19pub enum ExecError {
20    #[error("{0}")]
21    Kcl(#[from] Box<crate::KclErrorWithOutputs>),
22    #[error("Could not connect to engine: {0}")]
23    Connection(#[from] ConnectionError),
24    #[error("PNG snapshot could not be decoded: {0}")]
25    BadPng(String),
26    #[error("Bad export: {0}")]
27    BadExport(String),
28}
29
30impl From<KclErrorWithOutputs> for ExecError {
31    fn from(error: KclErrorWithOutputs) -> Self {
32        ExecError::Kcl(Box::new(error))
33    }
34}
35
36/// How did the KCL execution fail, with extra state.
37#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
38#[derive(Debug)]
39pub struct ExecErrorWithState {
40    pub error: ExecError,
41    pub exec_state: Option<crate::execution::ExecState>,
42}
43
44impl ExecErrorWithState {
45    #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
46    pub fn new(error: ExecError, exec_state: crate::execution::ExecState) -> Self {
47        Self {
48            error,
49            exec_state: Some(exec_state),
50        }
51    }
52}
53
54impl ExecError {
55    pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
56        let ExecError::Kcl(k) = &self else {
57            return None;
58        };
59        Some(&k.error)
60    }
61}
62
63impl From<ExecError> for ExecErrorWithState {
64    fn from(error: ExecError) -> Self {
65        Self {
66            error,
67            exec_state: None,
68        }
69    }
70}
71
72impl From<ConnectionError> for ExecErrorWithState {
73    fn from(error: ConnectionError) -> Self {
74        Self {
75            error: error.into(),
76            exec_state: None,
77        }
78    }
79}
80
81/// How did KCL client fail to connect to the engine
82#[derive(thiserror::Error, Debug)]
83pub enum ConnectionError {
84    #[error("Could not create a Zoo client: {0}")]
85    CouldNotMakeClient(anyhow::Error),
86    #[error("Could not establish connection to engine: {0}")]
87    Establishing(anyhow::Error),
88}
89
90#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
91#[ts(export)]
92#[serde(tag = "kind", rename_all = "snake_case")]
93pub enum KclError {
94    #[error("lexical: {0:?}")]
95    Lexical(KclErrorDetails),
96    #[error("syntax: {0:?}")]
97    Syntax(KclErrorDetails),
98    #[error("semantic: {0:?}")]
99    Semantic(KclErrorDetails),
100    #[error("import cycle: {0:?}")]
101    ImportCycle(KclErrorDetails),
102    #[error("type: {0:?}")]
103    Type(KclErrorDetails),
104    #[error("i/o: {0:?}")]
105    Io(KclErrorDetails),
106    #[error("unexpected: {0:?}")]
107    Unexpected(KclErrorDetails),
108    #[error("value already defined: {0:?}")]
109    ValueAlreadyDefined(KclErrorDetails),
110    #[error("undefined value: {0:?}")]
111    UndefinedValue(KclErrorDetails),
112    #[error("invalid expression: {0:?}")]
113    InvalidExpression(KclErrorDetails),
114    #[error("engine: {0:?}")]
115    Engine(KclErrorDetails),
116    #[error("internal error, please report to KittyCAD team: {0:?}")]
117    Internal(KclErrorDetails),
118}
119
120impl From<KclErrorWithOutputs> for KclError {
121    fn from(error: KclErrorWithOutputs) -> Self {
122        error.error
123    }
124}
125
126#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
127#[error("{error}")]
128#[ts(export)]
129#[serde(rename_all = "camelCase")]
130pub struct KclErrorWithOutputs {
131    pub error: KclError,
132    pub non_fatal: Vec<CompilationError>,
133    #[cfg(feature = "artifact-graph")]
134    pub operations: Vec<Operation>,
135    #[cfg(feature = "artifact-graph")]
136    pub artifact_commands: Vec<ArtifactCommand>,
137    #[cfg(feature = "artifact-graph")]
138    pub artifact_graph: ArtifactGraph,
139    pub filenames: IndexMap<ModuleId, ModulePath>,
140    pub source_files: IndexMap<ModuleId, ModuleSource>,
141    pub default_planes: Option<DefaultPlanes>,
142}
143
144impl KclErrorWithOutputs {
145    #[allow(clippy::too_many_arguments)]
146    pub fn new(
147        error: KclError,
148        non_fatal: Vec<CompilationError>,
149        #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
150        #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
151        #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
152        filenames: IndexMap<ModuleId, ModulePath>,
153        source_files: IndexMap<ModuleId, ModuleSource>,
154        default_planes: Option<DefaultPlanes>,
155    ) -> Self {
156        Self {
157            error,
158            non_fatal,
159            #[cfg(feature = "artifact-graph")]
160            operations,
161            #[cfg(feature = "artifact-graph")]
162            artifact_commands,
163            #[cfg(feature = "artifact-graph")]
164            artifact_graph,
165            filenames,
166            source_files,
167            default_planes,
168        }
169    }
170    pub fn no_outputs(error: KclError) -> Self {
171        Self {
172            error,
173            non_fatal: Default::default(),
174            #[cfg(feature = "artifact-graph")]
175            operations: Default::default(),
176            #[cfg(feature = "artifact-graph")]
177            artifact_commands: Default::default(),
178            #[cfg(feature = "artifact-graph")]
179            artifact_graph: Default::default(),
180            filenames: Default::default(),
181            source_files: Default::default(),
182            default_planes: Default::default(),
183        }
184    }
185    pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
186        let mut source_ranges = self.error.source_ranges();
187
188        // Pop off the first source range to get the filename.
189        let first_source_range = source_ranges
190            .pop()
191            .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
192
193        let source = self
194            .source_files
195            .get(&first_source_range.module_id())
196            .cloned()
197            .unwrap_or(ModuleSource {
198                source: code.to_string(),
199                path: self
200                    .filenames
201                    .get(&first_source_range.module_id())
202                    .cloned()
203                    .unwrap_or(ModulePath::Main),
204            });
205        let filename = source.path.to_string();
206        let kcl_source = source.source.to_string();
207
208        let mut related = Vec::new();
209        for source_range in source_ranges {
210            let module_id = source_range.module_id();
211            let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
212                source: code.to_string(),
213                path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
214            });
215            let error = self.error.override_source_ranges(vec![source_range]);
216            let report = Report {
217                error,
218                kcl_source: source.source.to_string(),
219                filename: source.path.to_string(),
220            };
221            related.push(report);
222        }
223
224        Ok(ReportWithOutputs {
225            error: self,
226            kcl_source,
227            filename,
228            related,
229        })
230    }
231}
232
233impl IntoDiagnostic for KclErrorWithOutputs {
234    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
235        let message = self.error.get_message();
236        let source_ranges = self.error.source_ranges();
237
238        source_ranges
239            .into_iter()
240            .map(|source_range| {
241                let source = self
242                    .source_files
243                    .get(&source_range.module_id())
244                    .cloned()
245                    .unwrap_or(ModuleSource {
246                        source: code.to_string(),
247                        path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
248                    });
249                let mut filename = source.path.to_string();
250                if !filename.starts_with("file://") {
251                    filename = format!("file:///{}", filename.trim_start_matches("/"));
252                }
253
254                let related_information = if let Ok(uri) = url::Url::parse(&filename) {
255                    Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
256                        location: tower_lsp::lsp_types::Location {
257                            uri,
258                            range: source_range.to_lsp_range(&source.source),
259                        },
260                        message: message.to_string(),
261                    }])
262                } else {
263                    None
264                };
265
266                Diagnostic {
267                    range: source_range.to_lsp_range(code),
268                    severity: Some(self.severity()),
269                    code: None,
270                    // TODO: this is neat we can pass a URL to a help page here for this specific error.
271                    code_description: None,
272                    source: Some("kcl".to_string()),
273                    related_information,
274                    message: message.clone(),
275                    tags: None,
276                    data: None,
277                }
278            })
279            .collect()
280    }
281
282    fn severity(&self) -> DiagnosticSeverity {
283        DiagnosticSeverity::ERROR
284    }
285}
286
287#[derive(thiserror::Error, Debug)]
288#[error("{}", self.error.error.get_message())]
289pub struct ReportWithOutputs {
290    pub error: KclErrorWithOutputs,
291    pub kcl_source: String,
292    pub filename: String,
293    pub related: Vec<Report>,
294}
295
296impl miette::Diagnostic for ReportWithOutputs {
297    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
298        let family = match self.error.error {
299            KclError::Lexical(_) => "Lexical",
300            KclError::Syntax(_) => "Syntax",
301            KclError::Semantic(_) => "Semantic",
302            KclError::ImportCycle(_) => "ImportCycle",
303            KclError::Type(_) => "Type",
304            KclError::Io(_) => "I/O",
305            KclError::Unexpected(_) => "Unexpected",
306            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
307            KclError::UndefinedValue(_) => "UndefinedValue",
308            KclError::InvalidExpression(_) => "InvalidExpression",
309            KclError::Engine(_) => "Engine",
310            KclError::Internal(_) => "Internal",
311        };
312        let error_string = format!("KCL {family} error");
313        Some(Box::new(error_string))
314    }
315
316    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
317        Some(&self.kcl_source)
318    }
319
320    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
321        let iter = self
322            .error
323            .error
324            .source_ranges()
325            .clone()
326            .into_iter()
327            .map(miette::SourceSpan::from)
328            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
329        Some(Box::new(iter))
330    }
331
332    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
333        let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
334        Some(Box::new(iter))
335    }
336}
337
338#[derive(thiserror::Error, Debug)]
339#[error("{}", self.error.get_message())]
340pub struct Report {
341    pub error: KclError,
342    pub kcl_source: String,
343    pub filename: String,
344}
345
346impl miette::Diagnostic for Report {
347    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
348        let family = match self.error {
349            KclError::Lexical(_) => "Lexical",
350            KclError::Syntax(_) => "Syntax",
351            KclError::Semantic(_) => "Semantic",
352            KclError::ImportCycle(_) => "ImportCycle",
353            KclError::Type(_) => "Type",
354            KclError::Io(_) => "I/O",
355            KclError::Unexpected(_) => "Unexpected",
356            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
357            KclError::UndefinedValue(_) => "UndefinedValue",
358            KclError::InvalidExpression(_) => "InvalidExpression",
359            KclError::Engine(_) => "Engine",
360            KclError::Internal(_) => "Internal",
361        };
362        let error_string = format!("KCL {family} error");
363        Some(Box::new(error_string))
364    }
365
366    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
367        Some(&self.kcl_source)
368    }
369
370    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
371        let iter = self
372            .error
373            .source_ranges()
374            .clone()
375            .into_iter()
376            .map(miette::SourceSpan::from)
377            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
378        Some(Box::new(iter))
379    }
380}
381
382#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
383#[serde(rename_all = "camelCase")]
384#[error("{message}")]
385#[ts(export)]
386pub struct KclErrorDetails {
387    #[label(collection, "Errors")]
388    pub source_ranges: Vec<SourceRange>,
389    pub backtrace: Vec<BacktraceItem>,
390    #[serde(rename = "msg")]
391    pub message: String,
392}
393
394impl KclErrorDetails {
395    pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
396        let backtrace = source_ranges
397            .iter()
398            .map(|s| BacktraceItem {
399                source_range: *s,
400                fn_name: None,
401            })
402            .collect();
403        KclErrorDetails {
404            source_ranges,
405            backtrace,
406            message,
407        }
408    }
409}
410
411impl KclError {
412    pub fn internal(message: String) -> KclError {
413        KclError::Internal(KclErrorDetails {
414            source_ranges: Default::default(),
415            backtrace: Default::default(),
416            message,
417        })
418    }
419
420    /// Get the error message.
421    pub fn get_message(&self) -> String {
422        format!("{}: {}", self.error_type(), self.message())
423    }
424
425    pub fn error_type(&self) -> &'static str {
426        match self {
427            KclError::Lexical(_) => "lexical",
428            KclError::Syntax(_) => "syntax",
429            KclError::Semantic(_) => "semantic",
430            KclError::ImportCycle(_) => "import cycle",
431            KclError::Type(_) => "type",
432            KclError::Io(_) => "i/o",
433            KclError::Unexpected(_) => "unexpected",
434            KclError::ValueAlreadyDefined(_) => "value already defined",
435            KclError::UndefinedValue(_) => "undefined value",
436            KclError::InvalidExpression(_) => "invalid expression",
437            KclError::Engine(_) => "engine",
438            KclError::Internal(_) => "internal",
439        }
440    }
441
442    pub fn source_ranges(&self) -> Vec<SourceRange> {
443        match &self {
444            KclError::Lexical(e) => e.source_ranges.clone(),
445            KclError::Syntax(e) => e.source_ranges.clone(),
446            KclError::Semantic(e) => e.source_ranges.clone(),
447            KclError::ImportCycle(e) => e.source_ranges.clone(),
448            KclError::Type(e) => e.source_ranges.clone(),
449            KclError::Io(e) => e.source_ranges.clone(),
450            KclError::Unexpected(e) => e.source_ranges.clone(),
451            KclError::ValueAlreadyDefined(e) => e.source_ranges.clone(),
452            KclError::UndefinedValue(e) => e.source_ranges.clone(),
453            KclError::InvalidExpression(e) => e.source_ranges.clone(),
454            KclError::Engine(e) => e.source_ranges.clone(),
455            KclError::Internal(e) => e.source_ranges.clone(),
456        }
457    }
458
459    /// Get the inner error message.
460    pub fn message(&self) -> &str {
461        match &self {
462            KclError::Lexical(e) => &e.message,
463            KclError::Syntax(e) => &e.message,
464            KclError::Semantic(e) => &e.message,
465            KclError::ImportCycle(e) => &e.message,
466            KclError::Type(e) => &e.message,
467            KclError::Io(e) => &e.message,
468            KclError::Unexpected(e) => &e.message,
469            KclError::ValueAlreadyDefined(e) => &e.message,
470            KclError::UndefinedValue(e) => &e.message,
471            KclError::InvalidExpression(e) => &e.message,
472            KclError::Engine(e) => &e.message,
473            KclError::Internal(e) => &e.message,
474        }
475    }
476
477    pub fn backtrace(&self) -> Vec<BacktraceItem> {
478        match self {
479            KclError::Lexical(e)
480            | KclError::Syntax(e)
481            | KclError::Semantic(e)
482            | KclError::ImportCycle(e)
483            | KclError::Type(e)
484            | KclError::Io(e)
485            | KclError::Unexpected(e)
486            | KclError::ValueAlreadyDefined(e)
487            | KclError::UndefinedValue(e)
488            | KclError::InvalidExpression(e)
489            | KclError::Engine(e)
490            | KclError::Internal(e) => e.backtrace.clone(),
491        }
492    }
493
494    pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
495        let mut new = self.clone();
496        match &mut new {
497            KclError::Lexical(e)
498            | KclError::Syntax(e)
499            | KclError::Semantic(e)
500            | KclError::ImportCycle(e)
501            | KclError::Type(e)
502            | KclError::Io(e)
503            | KclError::Unexpected(e)
504            | KclError::ValueAlreadyDefined(e)
505            | KclError::UndefinedValue(e)
506            | KclError::InvalidExpression(e)
507            | KclError::Engine(e)
508            | KclError::Internal(e) => {
509                e.backtrace = source_ranges
510                    .iter()
511                    .map(|s| BacktraceItem {
512                        source_range: *s,
513                        fn_name: None,
514                    })
515                    .collect();
516                e.source_ranges = source_ranges;
517            }
518        }
519
520        new
521    }
522
523    pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
524        let mut new = self.clone();
525        match &mut new {
526            KclError::Lexical(e)
527            | KclError::Syntax(e)
528            | KclError::Semantic(e)
529            | KclError::ImportCycle(e)
530            | KclError::Type(e)
531            | KclError::Io(e)
532            | KclError::Unexpected(e)
533            | KclError::ValueAlreadyDefined(e)
534            | KclError::UndefinedValue(e)
535            | KclError::InvalidExpression(e)
536            | KclError::Engine(e)
537            | KclError::Internal(e) => {
538                if let Some(item) = e.backtrace.last_mut() {
539                    item.fn_name = last_fn_name;
540                }
541            }
542        }
543
544        new
545    }
546
547    pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
548        let mut new = self.clone();
549        match &mut new {
550            KclError::Lexical(e)
551            | KclError::Syntax(e)
552            | KclError::Semantic(e)
553            | KclError::ImportCycle(e)
554            | KclError::Type(e)
555            | KclError::Io(e)
556            | KclError::Unexpected(e)
557            | KclError::ValueAlreadyDefined(e)
558            | KclError::UndefinedValue(e)
559            | KclError::InvalidExpression(e)
560            | KclError::Engine(e)
561            | KclError::Internal(e) => {
562                if let Some(item) = e.backtrace.last_mut() {
563                    item.fn_name = last_fn_name;
564                }
565                e.backtrace.push(BacktraceItem {
566                    source_range,
567                    fn_name: None,
568                });
569                e.source_ranges.push(source_range);
570            }
571        }
572
573        new
574    }
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
578#[serde(rename_all = "camelCase")]
579#[ts(export)]
580pub struct BacktraceItem {
581    pub source_range: SourceRange,
582    pub fn_name: Option<String>,
583}
584
585impl std::fmt::Display for BacktraceItem {
586    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587        if let Some(fn_name) = &self.fn_name {
588            write!(f, "{fn_name}: {:?}", self.source_range)
589        } else {
590            write!(f, "(fn): {:?}", self.source_range)
591        }
592    }
593}
594
595impl IntoDiagnostic for KclError {
596    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
597        let message = self.get_message();
598        let source_ranges = self.source_ranges();
599
600        // Limit to only errors in the top-level file.
601        let module_id = ModuleId::default();
602        let source_ranges = source_ranges
603            .iter()
604            .filter(|r| r.module_id() == module_id)
605            .collect::<Vec<_>>();
606
607        let mut diagnostics = Vec::new();
608        for source_range in &source_ranges {
609            diagnostics.push(Diagnostic {
610                range: source_range.to_lsp_range(code),
611                severity: Some(self.severity()),
612                code: None,
613                // TODO: this is neat we can pass a URL to a help page here for this specific error.
614                code_description: None,
615                source: Some("kcl".to_string()),
616                related_information: None,
617                message: message.clone(),
618                tags: None,
619                data: None,
620            });
621        }
622
623        diagnostics
624    }
625
626    fn severity(&self) -> DiagnosticSeverity {
627        DiagnosticSeverity::ERROR
628    }
629}
630
631/// This is different than to_string() in that it will serialize the Error
632/// the struct as JSON so we can deserialize it on the js side.
633impl From<KclError> for String {
634    fn from(error: KclError) -> Self {
635        serde_json::to_string(&error).unwrap()
636    }
637}
638
639impl From<String> for KclError {
640    fn from(error: String) -> Self {
641        serde_json::from_str(&error).unwrap()
642    }
643}
644
645#[cfg(feature = "pyo3")]
646impl From<pyo3::PyErr> for KclError {
647    fn from(error: pyo3::PyErr) -> Self {
648        KclError::Internal(KclErrorDetails {
649            source_ranges: vec![],
650            backtrace: Default::default(),
651            message: error.to_string(),
652        })
653    }
654}
655
656#[cfg(feature = "pyo3")]
657impl From<KclError> for pyo3::PyErr {
658    fn from(error: KclError) -> Self {
659        pyo3::exceptions::PyException::new_err(error.to_string())
660    }
661}
662
663/// An error which occurred during parsing, etc.
664#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq)]
665#[ts(export)]
666pub struct CompilationError {
667    #[serde(rename = "sourceRange")]
668    pub source_range: SourceRange,
669    pub message: String,
670    pub suggestion: Option<Suggestion>,
671    pub severity: Severity,
672    pub tag: Tag,
673}
674
675impl CompilationError {
676    pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
677        CompilationError {
678            source_range,
679            message: message.to_string(),
680            suggestion: None,
681            severity: Severity::Error,
682            tag: Tag::None,
683        }
684    }
685
686    pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
687        CompilationError {
688            source_range,
689            message: message.to_string(),
690            suggestion: None,
691            severity: Severity::Fatal,
692            tag: Tag::None,
693        }
694    }
695
696    pub(crate) fn with_suggestion(
697        self,
698        suggestion_title: impl ToString,
699        suggestion_insert: impl ToString,
700        // Will use the error source range if none is supplied
701        source_range: Option<SourceRange>,
702        tag: Tag,
703    ) -> CompilationError {
704        CompilationError {
705            suggestion: Some(Suggestion {
706                title: suggestion_title.to_string(),
707                insert: suggestion_insert.to_string(),
708                source_range: source_range.unwrap_or(self.source_range),
709            }),
710            tag,
711            ..self
712        }
713    }
714
715    #[cfg(test)]
716    pub fn apply_suggestion(&self, src: &str) -> Option<String> {
717        let suggestion = self.suggestion.as_ref()?;
718        Some(format!(
719            "{}{}{}",
720            &src[0..suggestion.source_range.start()],
721            suggestion.insert,
722            &src[suggestion.source_range.end()..]
723        ))
724    }
725}
726
727impl From<CompilationError> for KclErrorDetails {
728    fn from(err: CompilationError) -> Self {
729        let backtrace = vec![BacktraceItem {
730            source_range: err.source_range,
731            fn_name: None,
732        }];
733        KclErrorDetails {
734            source_ranges: vec![err.source_range],
735            backtrace,
736            message: err.message,
737        }
738    }
739}
740
741#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
742#[ts(export)]
743pub enum Severity {
744    Warning,
745    Error,
746    Fatal,
747}
748
749impl Severity {
750    pub fn is_err(self) -> bool {
751        match self {
752            Severity::Warning => false,
753            Severity::Error | Severity::Fatal => true,
754        }
755    }
756}
757
758#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
759#[ts(export)]
760pub enum Tag {
761    Deprecated,
762    Unnecessary,
763    None,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq, JsonSchema)]
767#[ts(export)]
768pub struct Suggestion {
769    pub title: String,
770    pub insert: String,
771    pub source_range: SourceRange,
772}
773
774pub type LspSuggestion = (Suggestion, tower_lsp::lsp_types::Range);
775
776impl Suggestion {
777    pub fn to_lsp_edit(&self, code: &str) -> LspSuggestion {
778        let range = self.source_range.to_lsp_range(code);
779        (self.clone(), range)
780    }
781}