Skip to content

Commit 06a3422

Browse files
refactor and move lsp types to server boundary (#322)
1 parent edc44aa commit 06a3422

File tree

10 files changed

+428
-234
lines changed

10 files changed

+428
-234
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/djls-server/src/client.rs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
//! LSP client identification for client-specific behavioral overrides.
2+
3+
use djls_source::PositionEncoding;
4+
use tower_lsp_server::lsp_types;
5+
6+
use crate::ext::ClientInfoExt;
7+
use crate::ext::PositionEncodingKindExt;
8+
9+
/// LSP client identification for client-specific behavioral overrides.
10+
///
11+
/// Most clients work fine with standard LSP behavior, but some require
12+
/// specific workarounds (e.g., language ID mappings, capability quirks).
13+
#[derive(Clone, Copy, Debug, PartialEq)]
14+
pub enum Client {
15+
/// Standard LSP client behavior (no special overrides needed)
16+
Default,
17+
/// Sublime Text LSP - uses "html" language ID for Django templates
18+
SublimeText,
19+
}
20+
21+
#[derive(Debug, Clone, Copy)]
22+
pub struct ClientCapabilities {
23+
client: Client,
24+
position_encoding: PositionEncoding,
25+
pull_diagnostics: bool,
26+
snippets: bool,
27+
}
28+
29+
impl ClientCapabilities {
30+
pub fn negotiate(
31+
capabilities: &lsp_types::ClientCapabilities,
32+
client_info: Option<&lsp_types::ClientInfo>,
33+
) -> Self {
34+
let pull_diagnostics = capabilities
35+
.text_document
36+
.as_ref()
37+
.and_then(|text_doc| text_doc.diagnostic.as_ref())
38+
.is_some();
39+
40+
let snippets = capabilities
41+
.text_document
42+
.as_ref()
43+
.and_then(|text_document| text_document.completion.as_ref())
44+
.and_then(|completion| completion.completion_item.as_ref())
45+
.and_then(|completion_item| completion_item.snippet_support)
46+
.unwrap_or(false);
47+
48+
let client_encodings = capabilities
49+
.general
50+
.as_ref()
51+
.and_then(|general| general.position_encodings.as_ref())
52+
.map_or(&[][..], |kinds| kinds.as_slice());
53+
54+
let position_encoding = [
55+
PositionEncoding::Utf8,
56+
PositionEncoding::Utf32,
57+
PositionEncoding::Utf16,
58+
]
59+
.into_iter()
60+
.find(|&preferred| {
61+
client_encodings
62+
.iter()
63+
.any(|kind| kind.to_position_encoding() == Some(preferred))
64+
})
65+
.unwrap_or(PositionEncoding::Utf16);
66+
67+
let client = client_info.to_client();
68+
69+
Self {
70+
client,
71+
position_encoding,
72+
pull_diagnostics,
73+
snippets,
74+
}
75+
}
76+
77+
#[must_use]
78+
pub fn position_encoding(self) -> PositionEncoding {
79+
self.position_encoding
80+
}
81+
82+
#[must_use]
83+
pub fn client(self) -> Client {
84+
self.client
85+
}
86+
87+
#[must_use]
88+
pub fn supports_pull_diagnostics(self) -> bool {
89+
self.pull_diagnostics
90+
}
91+
92+
#[must_use]
93+
pub fn supports_snippets(self) -> bool {
94+
self.snippets
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use std::str::FromStr;
101+
102+
use djls_source::FileKind;
103+
104+
use super::*;
105+
use crate::ext::TextDocumentItemExt;
106+
107+
#[test]
108+
fn test_negotiate_prefers_utf8_when_available() {
109+
let capabilities = lsp_types::ClientCapabilities {
110+
general: Some(lsp_types::GeneralClientCapabilities {
111+
position_encodings: Some(vec![
112+
lsp_types::PositionEncodingKind::new("utf-16"),
113+
lsp_types::PositionEncodingKind::new("utf-8"),
114+
lsp_types::PositionEncodingKind::new("utf-32"),
115+
]),
116+
..Default::default()
117+
}),
118+
..Default::default()
119+
};
120+
assert_eq!(
121+
ClientCapabilities::negotiate(&capabilities, None).position_encoding(),
122+
PositionEncoding::Utf8
123+
);
124+
}
125+
126+
#[test]
127+
fn test_negotiate_prefers_utf32_over_utf16() {
128+
let capabilities = lsp_types::ClientCapabilities {
129+
general: Some(lsp_types::GeneralClientCapabilities {
130+
position_encodings: Some(vec![
131+
lsp_types::PositionEncodingKind::new("utf-16"),
132+
lsp_types::PositionEncodingKind::new("utf-32"),
133+
]),
134+
..Default::default()
135+
}),
136+
..Default::default()
137+
};
138+
assert_eq!(
139+
ClientCapabilities::negotiate(&capabilities, None).position_encoding(),
140+
PositionEncoding::Utf32
141+
);
142+
}
143+
144+
#[test]
145+
fn test_negotiate_fallback_with_unsupported_encodings() {
146+
let capabilities = lsp_types::ClientCapabilities {
147+
general: Some(lsp_types::GeneralClientCapabilities {
148+
position_encodings: Some(vec![
149+
lsp_types::PositionEncodingKind::new("ascii"),
150+
lsp_types::PositionEncodingKind::new("utf-7"),
151+
]),
152+
..Default::default()
153+
}),
154+
..Default::default()
155+
};
156+
assert_eq!(
157+
ClientCapabilities::negotiate(&capabilities, None).position_encoding(),
158+
PositionEncoding::Utf16
159+
);
160+
}
161+
162+
#[test]
163+
fn test_negotiate_fallback_with_no_capabilities() {
164+
let capabilities = lsp_types::ClientCapabilities::default();
165+
assert_eq!(
166+
ClientCapabilities::negotiate(&capabilities, None).position_encoding(),
167+
PositionEncoding::Utf16
168+
);
169+
}
170+
171+
#[test]
172+
fn test_negotiate_detects_sublime_client() {
173+
let capabilities = lsp_types::ClientCapabilities::default();
174+
let client_info = lsp_types::ClientInfo {
175+
name: "Sublime Text LSP".to_string(),
176+
version: Some("1.0.0".to_string()),
177+
};
178+
assert_eq!(
179+
ClientCapabilities::negotiate(&capabilities, Some(&client_info)).client(),
180+
Client::SublimeText
181+
);
182+
}
183+
184+
#[test]
185+
fn test_negotiate_defaults_to_default_client() {
186+
let capabilities = lsp_types::ClientCapabilities::default();
187+
let client_info = lsp_types::ClientInfo {
188+
name: "Other Client".to_string(),
189+
version: None,
190+
};
191+
assert_eq!(
192+
ClientCapabilities::negotiate(&capabilities, Some(&client_info)).client(),
193+
Client::Default
194+
);
195+
}
196+
197+
#[test]
198+
fn test_map_language_id_sublime_html_to_template() {
199+
let capabilities = lsp_types::ClientCapabilities::default();
200+
let client_info = lsp_types::ClientInfo {
201+
name: "Sublime Text LSP".to_string(),
202+
version: None,
203+
};
204+
let client_caps = ClientCapabilities::negotiate(&capabilities, Some(&client_info));
205+
let doc = lsp_types::TextDocumentItem {
206+
uri: lsp_types::Uri::from_str("file:///test.html").unwrap(),
207+
language_id: "html".to_string(),
208+
version: 1,
209+
text: String::new(),
210+
};
211+
assert_eq!(
212+
doc.language_id_to_file_kind(client_caps.client()),
213+
FileKind::Template
214+
);
215+
}
216+
217+
#[test]
218+
fn test_map_language_id_default_html_to_other() {
219+
let capabilities = lsp_types::ClientCapabilities::default();
220+
let client_caps = ClientCapabilities::negotiate(&capabilities, None);
221+
let doc = lsp_types::TextDocumentItem {
222+
uri: lsp_types::Uri::from_str("file:///test.html").unwrap(),
223+
language_id: "html".to_string(),
224+
version: 1,
225+
text: String::new(),
226+
};
227+
assert_eq!(
228+
doc.language_id_to_file_kind(client_caps.client()),
229+
FileKind::Other
230+
);
231+
}
232+
233+
#[test]
234+
fn test_map_language_id_django_html_always_template() {
235+
let capabilities = lsp_types::ClientCapabilities::default();
236+
let client_caps = ClientCapabilities::negotiate(&capabilities, None);
237+
let doc = lsp_types::TextDocumentItem {
238+
uri: lsp_types::Uri::from_str("file:///test.html").unwrap(),
239+
language_id: "django-html".to_string(),
240+
version: 1,
241+
text: String::new(),
242+
};
243+
assert_eq!(
244+
doc.language_id_to_file_kind(client_caps.client()),
245+
FileKind::Template
246+
);
247+
}
248+
}

0 commit comments

Comments
 (0)