1use lsp_types::{OneOf, ServerCapabilities};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SemanticBackendKind {
13 Lsp,
15 JetBrains,
17 Editor,
19}
20
21impl SemanticBackendKind {
22 pub fn as_str(self) -> &'static str {
23 match self {
24 Self::Lsp => "lsp",
25 Self::JetBrains => "jetbrains",
26 Self::Editor => "editor",
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub struct SemanticCapabilities {
34 pub definition: bool,
35 pub declaration: bool,
36 pub references: bool,
37 pub implementations: bool,
38 pub type_hierarchy: bool,
39 pub call_hierarchy: bool,
40 pub document_symbols: bool,
41 pub rename: bool,
42}
43
44impl SemanticCapabilities {
45 pub const ALL: Self = Self {
47 definition: true,
48 declaration: true,
49 references: true,
50 implementations: true,
51 type_hierarchy: true,
52 call_hierarchy: true,
53 document_symbols: true,
54 rename: true,
55 };
56
57 pub fn from_server(caps: &ServerCapabilities) -> Self {
60 fn one_of<T>(p: Option<&OneOf<bool, T>>) -> bool {
61 matches!(p, Some(OneOf::Left(true) | OneOf::Right(_)))
62 }
63 Self {
64 definition: one_of(caps.definition_provider.as_ref()),
65 declaration: caps
66 .declaration_provider
67 .as_ref()
68 .is_some_and(|p| !matches!(p, lsp_types::DeclarationCapability::Simple(false))),
69 references: one_of(caps.references_provider.as_ref()),
70 implementations: caps.implementation_provider.as_ref().is_some_and(|p| {
71 !matches!(
72 p,
73 lsp_types::ImplementationProviderCapability::Simple(false)
74 )
75 }),
76 type_hierarchy: false,
79 call_hierarchy: caps.call_hierarchy_provider.as_ref().is_some_and(|p| {
80 !matches!(p, lsp_types::CallHierarchyServerCapability::Simple(false))
81 }),
82 document_symbols: one_of(caps.document_symbol_provider.as_ref()),
83 rename: one_of(caps.rename_provider.as_ref()),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SemanticBackendInfo {
91 pub kind: SemanticBackendKind,
92 pub server_name: Option<String>,
94 pub server_version: Option<String>,
95 pub capabilities: SemanticCapabilities,
96 pub utf8_positions: bool,
100}
101
102pub fn encode_column(line_text: &str, byte_col: usize, utf8_positions: bool) -> u32 {
106 let mut col = byte_col.min(line_text.len());
107 while !line_text.is_char_boundary(col) {
108 col -= 1;
109 }
110 let prefix = &line_text[..col];
111 let units = if utf8_positions {
112 prefix.len()
113 } else {
114 prefix.encode_utf16().count()
115 };
116 u32::try_from(units).unwrap_or(u32::MAX)
117}
118
119pub fn compact_server_version(raw: &str) -> String {
125 const MAX: usize = 64;
126 const MAX_PARSED: usize = 64 * 1024;
128 let raw = raw.trim();
129 if raw.len() <= MAX && !raw.contains(['{', '\n', '"']) {
130 return raw.to_string();
131 }
132 let digest = blake3::hash(raw.as_bytes()).to_hex();
133 let release = (raw.len() <= MAX_PARSED)
134 .then(|| serde_json::from_str::<serde_json::Value>(raw).ok())
135 .flatten()
136 .and_then(|v| v.get("Version")?.as_str().map(str::to_string))
137 .filter(|v| v.len() <= MAX && !v.contains(['{', '\n', '"']));
138 match release {
139 Some(v) => format!("{v}+{}", &digest[..12]),
140 None => format!("build+{}", &digest[..12]),
141 }
142}
143
144impl SemanticBackendInfo {
145 pub fn identity(&self) -> String {
148 format!(
149 "{}:{}@{}",
150 self.kind.as_str(),
151 self.server_name.as_deref().unwrap_or("unknown"),
152 self.server_version.as_deref().unwrap_or("unknown")
153 )
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use lsp_types::{ImplementationProviderCapability, OneOf, ServerCapabilities};
161
162 #[test]
163 fn negotiated_capabilities_follow_the_server_answer() {
164 let caps = ServerCapabilities {
165 definition_provider: Some(OneOf::Left(true)),
166 references_provider: Some(OneOf::Left(false)),
167 implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
168 ..Default::default()
169 };
170 let negotiated = SemanticCapabilities::from_server(&caps);
171 assert!(negotiated.definition);
172 assert!(!negotiated.references, "explicit false must be honoured");
173 assert!(negotiated.implementations);
174 assert!(!negotiated.rename, "absent provider means unsupported");
175 }
176
177 #[test]
178 fn byte_columns_are_encoded_per_negotiated_encoding() {
179 let line = "รค๐.save()";
181 let save = line.find("save").unwrap(); assert_eq!(encode_column(line, save, true), 7);
183 assert_eq!(encode_column(line, save, false), 4);
184 assert_eq!(
185 encode_column(line, 3, false),
186 1,
187 "inside ๐ snaps to its start"
188 );
189 assert_eq!(encode_column(line, 999, true), line.len() as u32);
190 }
191
192 #[test]
193 fn server_versions_are_bounded_but_stay_unique_per_build() {
194 assert_eq!(compact_server_version("1.15.0"), "1.15.0");
195 let a =
197 r#"{"GoVersion":"go1.26","Deps":[{"Path":"x","Version":"v1"}],"Version":"v0.23.0"}"#;
198 let b =
199 r#"{"GoVersion":"go1.27","Deps":[{"Path":"x","Version":"v1"}],"Version":"v0.23.0"}"#;
200 let (ca, cb) = (compact_server_version(a), compact_server_version(b));
201 assert!(ca.starts_with("v0.23.0+") && ca.len() <= 24, "{ca}");
202 assert_ne!(ca, cb, "a different build keeps a different identity");
203 let blob = "x".repeat(500);
204 assert!(compact_server_version(&blob).starts_with("build+"));
205 }
206}