Skip to main content

wraith_lsp/
types.rs

1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter};
3use std::path::{Path, PathBuf};
4
5use lsp_types::{Diagnostic, Range};
6use serde_json::Value;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct LspServerConfig {
10    pub name: String,
11    pub command: String,
12    pub args: Vec<String>,
13    pub env: BTreeMap<String, String>,
14    pub workspace_root: PathBuf,
15    pub initialization_options: Option<Value>,
16    pub extension_to_language: BTreeMap<String, String>,
17}
18
19impl LspServerConfig {
20    #[must_use]
21    pub fn language_id_for(&self, path: &Path) -> Option<&str> {
22        let extension = normalize_extension(path.extension()?.to_string_lossy().as_ref());
23        self.extension_to_language
24            .get(&extension)
25            .map(String::as_str)
26    }
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct FileDiagnostics {
31    pub path: PathBuf,
32    pub uri: String,
33    pub diagnostics: Vec<Diagnostic>,
34}
35
36#[derive(Debug, Clone, Default, PartialEq)]
37pub struct WorkspaceDiagnostics {
38    pub files: Vec<FileDiagnostics>,
39}
40
41impl WorkspaceDiagnostics {
42    #[must_use]
43    pub fn is_empty(&self) -> bool {
44        self.files.is_empty()
45    }
46
47    #[must_use]
48    pub fn total_diagnostics(&self) -> usize {
49        self.files.iter().map(|file| file.diagnostics.len()).sum()
50    }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SymbolLocation {
55    pub path: PathBuf,
56    pub range: Range,
57}
58
59impl SymbolLocation {
60    #[must_use]
61    pub fn start_line(&self) -> u32 {
62        self.range.start.line + 1
63    }
64
65    #[must_use]
66    pub fn start_character(&self) -> u32 {
67        self.range.start.character + 1
68    }
69}
70
71impl Display for SymbolLocation {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        write!(
74            f,
75            "{}:{}:{}",
76            self.path.display(),
77            self.start_line(),
78            self.start_character()
79        )
80    }
81}
82
83#[derive(Debug, Clone, Default, PartialEq)]
84pub struct LspContextEnrichment {
85    pub file_path: PathBuf,
86    pub diagnostics: WorkspaceDiagnostics,
87    pub definitions: Vec<SymbolLocation>,
88    pub references: Vec<SymbolLocation>,
89}
90
91impl LspContextEnrichment {
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.diagnostics.is_empty() && self.definitions.is_empty() && self.references.is_empty()
95    }
96
97    #[must_use]
98    pub fn render_prompt_section(&self) -> String {
99        const MAX_RENDERED_DIAGNOSTICS: usize = 12;
100        const MAX_RENDERED_LOCATIONS: usize = 12;
101
102        let mut lines = vec!["# LSP context".to_string()];
103        lines.push(format!(" - Focus file: {}", self.file_path.display()));
104        lines.push(format!(
105            " - Workspace diagnostics: {} across {} file(s)",
106            self.diagnostics.total_diagnostics(),
107            self.diagnostics.files.len()
108        ));
109
110        if !self.diagnostics.files.is_empty() {
111            lines.push(String::new());
112            lines.push("Diagnostics:".to_string());
113            let mut rendered = 0usize;
114            for file in &self.diagnostics.files {
115                for diagnostic in &file.diagnostics {
116                    if rendered == MAX_RENDERED_DIAGNOSTICS {
117                        lines.push(" - Additional diagnostics omitted for brevity.".to_string());
118                        break;
119                    }
120                    let severity = diagnostic_severity_label(diagnostic.severity);
121                    lines.push(format!(
122                        " - {}:{}:{} [{}] {}",
123                        file.path.display(),
124                        diagnostic.range.start.line + 1,
125                        diagnostic.range.start.character + 1,
126                        severity,
127                        diagnostic.message.replace('\n', " ")
128                    ));
129                    rendered += 1;
130                }
131                if rendered == MAX_RENDERED_DIAGNOSTICS {
132                    break;
133                }
134            }
135        }
136
137        if !self.definitions.is_empty() {
138            lines.push(String::new());
139            lines.push("Definitions:".to_string());
140            lines.extend(
141                self.definitions
142                    .iter()
143                    .take(MAX_RENDERED_LOCATIONS)
144                    .map(|location| format!(" - {location}")),
145            );
146            if self.definitions.len() > MAX_RENDERED_LOCATIONS {
147                lines.push(" - Additional definitions omitted for brevity.".to_string());
148            }
149        }
150
151        if !self.references.is_empty() {
152            lines.push(String::new());
153            lines.push("References:".to_string());
154            lines.extend(
155                self.references
156                    .iter()
157                    .take(MAX_RENDERED_LOCATIONS)
158                    .map(|location| format!(" - {location}")),
159            );
160            if self.references.len() > MAX_RENDERED_LOCATIONS {
161                lines.push(" - Additional references omitted for brevity.".to_string());
162            }
163        }
164
165        lines.join("\n")
166    }
167}
168
169#[must_use]
170pub(crate) fn normalize_extension(extension: &str) -> String {
171    if extension.starts_with('.') {
172        extension.to_ascii_lowercase()
173    } else {
174        format!(".{}", extension.to_ascii_lowercase())
175    }
176}
177
178fn diagnostic_severity_label(severity: Option<lsp_types::DiagnosticSeverity>) -> &'static str {
179    match severity {
180        Some(lsp_types::DiagnosticSeverity::ERROR) => "error",
181        Some(lsp_types::DiagnosticSeverity::WARNING) => "warning",
182        Some(lsp_types::DiagnosticSeverity::INFORMATION) => "info",
183        Some(lsp_types::DiagnosticSeverity::HINT) => "hint",
184        _ => "unknown",
185    }
186}