1use crossbeam_channel::{Receiver, Sender, bounded};
2use css_ast::{CssAtomSet, StyleSheet, Visitable};
3use css_lexer::Lexer;
4use css_parse::{Arena, Diagnostic, Parser, ParserReturn, Severity};
5use csskit_highlight::{SemanticKind, SemanticModifier, TokenHighlighter};
6use dashmap::DashMap;
7use itertools::Itertools;
8use lsp_service::{Client, ErrorCode, Handler, PositionEncoding, ResponseError};
9use lsp_types::{SemanticToken, Uri, notification::PublishDiagnostics};
10use ropey::Rope;
11use std::{
12 sync::{Arc, OnceLock},
13 thread::{Builder, JoinHandle},
14};
15use strum::VariantNames;
16use tracing::{instrument, trace, trace_span, warn};
17
18#[derive(Debug)]
19enum FileCall {
20 RopeChange(Rope, i32),
21 Highlight,
22}
23
24#[derive(Debug)]
25enum FileReturn {
26 Tokens(Vec<SemanticToken>),
27}
28
29#[derive(Debug)]
30pub struct File {
31 pub content: Rope,
32 #[allow(dead_code)]
33 thread: JoinHandle<()>,
34 sender: Sender<FileCall>,
35 receiver: Receiver<FileReturn>,
36}
37
38impl File {
39 fn new(uri: Uri, encoding: PositionEncoding, client: Client) -> Self {
40 let (sender, read_receiver) = bounded::<FileCall>(0);
41 let (write_sender, receiver) = bounded::<FileReturn>(0);
42 Self {
43 content: Rope::new(),
44 sender,
45 receiver,
46 thread: Builder::new()
47 .name("LspDocumentHandler".into())
48 .spawn(move || {
49 let mut alloc = Arena::default();
50 let mut rope = Rope::new();
51 let mut string: String = "".into();
52 let lexer = Lexer::new(&CssAtomSet::ATOMS, "");
53 let mut result: ParserReturn<'_, StyleSheet<'_>> =
54 Parser::new(&alloc, "", lexer).parse_entirely::<StyleSheet>();
55 while let Ok(call) = read_receiver.recv() {
56 match call {
57 FileCall::RopeChange(new_rope, version) => {
58 let span = trace_span!("Parsing document");
59 let _ = span.enter();
60 alloc.reset();
63 rope = new_rope;
64 string = String::from(&rope);
65 let lexer = Lexer::new(&CssAtomSet::ATOMS, &string);
66 result = Parser::new(&alloc, &string, lexer).parse_entirely::<StyleSheet>();
67 let diagnostics = result
71 .errors
72 .iter()
73 .map(|diagnostic| to_lsp_diagnostic(diagnostic, &string, &rope, encoding))
74 .collect();
75 let params = lsp_types::PublishDiagnosticsParams {
76 uri: uri.clone(),
77 diagnostics,
78 version: Some(version),
79 };
80 if let Err(e) = client.notify::<PublishDiagnostics>(params) {
81 warn!("Failed to publish diagnostics, connection closed: {e:?}");
82 }
83 }
84 FileCall::Highlight => {
85 let span = trace_span!("Highlighting document");
86 let _ = span.enter();
87 let mut highlighter = TokenHighlighter::new();
88 let mut tokens = vec![];
89 if let Some(stylesheet) = &result.output {
90 let _ = stylesheet.accept(&mut highlighter);
91 let mut current_line = 0;
92 let mut current_start = 0;
93 tokens = highlighter
94 .highlights()
95 .sorted_by(|a, b| Ord::cmp(&a.span(), &b.span()))
96 .map(|h| {
97 let span = h.span();
98 let start =
99 encoding.position(&rope, rope.byte_to_char(span.start().into()));
100 let delta_line = start.line - current_line;
101 let delta_start = if delta_line == 0 {
102 start.character - current_start
103 } else {
104 start.character
105 };
106 current_line = start.line;
107 current_start = start.character;
108 SemanticToken {
109 delta_line,
110 delta_start,
111 length: encoding.len_of(span.str_slice(&string)),
112 token_type: h.kind().bits() as u32,
113 token_modifiers_bitset: h.modifier().bits() as u32,
114 }
115 })
116 .collect();
117 }
118 write_sender.send(FileReturn::Tokens(tokens)).ok();
119 }
120 }
121 }
122 })
123 .expect("Failed to document thread Reader"),
124 }
125 }
126
127 fn commit(&mut self, rope: Rope, version: i32) {
128 self.content = rope;
129 self.sender.send(FileCall::RopeChange(self.content.clone(), version)).unwrap();
130 }
131
132 #[instrument]
133 fn get_tokens(&self) -> Vec<SemanticToken> {
134 self.sender.send(FileCall::Highlight).unwrap();
135 if let Ok(FileReturn::Tokens(tokens)) = self.receiver.recv() {
136 return tokens;
137 }
138 vec![]
139 }
140}
141
142fn to_lsp_diagnostic(
143 diagnostic: &Diagnostic,
144 source: &str,
145 rope: &Rope,
146 encoding: PositionEncoding,
147) -> lsp_types::Diagnostic {
148 let span = diagnostic.span();
149 let meta = diagnostic.meta(source);
150 let position = |offset: usize| encoding.position(rope, rope.byte_to_char(offset.min(source.len())));
151 lsp_types::Diagnostic {
152 range: lsp_types::Range::new(position(span.start().into()), position(span.end().into())),
153 severity: Some(match diagnostic.severity {
154 Severity::Error => lsp_types::DiagnosticSeverity::ERROR,
155 Severity::Warning => lsp_types::DiagnosticSeverity::WARNING,
156 Severity::Advice => lsp_types::DiagnosticSeverity::INFORMATION,
157 }),
158 code: Some(lsp_types::NumberOrString::String(meta.code.into())),
159 source: Some("csskit".into()),
160 message: meta.message,
161 ..Default::default()
162 }
163}
164
165#[derive(Debug)]
166pub struct LSPService {
167 version: String,
168 client: Client,
169 files: Arc<DashMap<Uri, File>>,
170 encoding: OnceLock<PositionEncoding>,
171}
172
173impl LSPService {
174 pub fn new(version: &'static str, client: Client) -> Self {
175 Self { version: version.into(), client, files: Arc::new(DashMap::new()), encoding: OnceLock::new() }
176 }
177
178 fn encoding(&self) -> PositionEncoding {
179 self.encoding.get().copied().unwrap_or_default()
180 }
181}
182
183impl Handler for LSPService {
184 #[instrument]
185 fn initialized(&self) -> bool {
186 self.encoding.get().is_some()
187 }
188
189 #[instrument]
190 fn initialize(&self, req: lsp_types::InitializeParams) -> Result<lsp_types::InitializeResult, ResponseError> {
191 let encoding = *self.encoding.get_or_init(|| PositionEncoding::negotiate(&req.capabilities));
192 Ok(lsp_types::InitializeResult {
193 capabilities: lsp_types::ServerCapabilities {
194 position_encoding: Some(encoding.kind()),
195 text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
196 lsp_types::TextDocumentSyncOptions {
197 open_close: Some(true),
198 change: Some(lsp_types::TextDocumentSyncKind::INCREMENTAL),
199 will_save: Some(true),
200 will_save_wait_until: Some(false),
201 save: Some(lsp_types::TextDocumentSyncSaveOptions::Supported(false)),
202 },
203 )),
204 completion_provider: Some(lsp_types::CompletionOptions {
208 resolve_provider: None,
209 trigger_characters: Some(vec![".".into(), ":".into(), "@".into(), "#".into(), "-".into()]),
210 all_commit_characters: None,
211 work_done_progress_options: lsp_types::WorkDoneProgressOptions { work_done_progress: None },
212 completion_item: None,
213 }),
214 semantic_tokens_provider: Some(lsp_types::SemanticTokensServerCapabilities::SemanticTokensOptions(
236 lsp_types::SemanticTokensOptions {
237 work_done_progress_options: lsp_types::WorkDoneProgressOptions {
238 work_done_progress: Some(false),
239 },
240 legend: lsp_types::SemanticTokensLegend {
241 token_types: SemanticKind::VARIANTS
242 .iter()
243 .map(|v| lsp_types::SemanticTokenType::new(v))
244 .collect(),
245 token_modifiers: SemanticModifier::VARIANTS
246 .iter()
247 .map(|v| lsp_types::SemanticTokenModifier::new(v))
248 .collect(),
249 },
250 range: Some(false),
251 full: Some(lsp_types::SemanticTokensFullOptions::Delta { delta: Some(true) }),
252 },
253 )),
254 ..Default::default()
262 },
263 server_info: Some(lsp_types::ServerInfo {
264 name: String::from("csskit-lsp"),
265 version: Some(self.version.clone()),
266 }),
267 offset_encoding: None,
268 })
269 }
270
271 #[instrument]
272 fn semantic_tokens_full_request(
273 &self,
274 req: lsp_types::SemanticTokensParams,
275 ) -> Result<Option<lsp_types::SemanticTokensResult>, ResponseError> {
276 let uri = req.text_document.uri;
277 trace!("Asked for SemanticTokens for {:?}", &uri);
278 if let Some(document) = self.files.get(&uri) {
279 let data = document.get_tokens();
280 Ok(Some(lsp_types::SemanticTokensResult::Tokens(lsp_types::SemanticTokens { result_id: None, data })))
281 } else {
282 Err(ErrorCode::InternalError.into())
283 }
284 }
285
286 #[instrument]
287 fn completion(
288 &self,
289 req: lsp_types::CompletionParams,
290 ) -> Result<Option<lsp_types::CompletionResponse>, ResponseError> {
291 Ok(None)
295 }
296
297 #[instrument]
298 fn on_did_open_text_document(&self, req: lsp_types::DidOpenTextDocumentParams) {
299 let uri = req.text_document.uri;
300 let mut doc = File::new(uri.clone(), self.encoding(), self.client.clone());
301 let rope = Rope::from_str(&req.text_document.text);
302 trace!("comitting new document {:?} {:?}", &uri, rope);
303 doc.commit(rope, req.text_document.version);
304 self.files.clone().insert(uri, doc);
305 }
306
307 #[instrument]
308 fn on_did_change_text_document(&self, req: lsp_types::DidChangeTextDocumentParams) {
309 let uri = req.text_document.uri;
310 let encoding = self.encoding();
311 if let Some(mut file) = self.files.clone().get_mut(&uri) {
312 let mut rope = file.content.clone();
313 for change in req.content_changes {
314 let (start, end) = match change.range {
315 Some(range) => (encoding.char_index(&rope, range.start), encoding.char_index(&rope, range.end)),
316 None => (0, rope.len_chars()),
317 };
318 rope.remove(start..end.max(start));
319 rope.insert(start, &change.text);
320 }
321 file.commit(rope, req.text_document.version)
322 }
323 }
324}