Skip to main content

csskit_lsp/
service.rs

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								// TODO! we should be able to optimize this by parsing a subset of the tree and mutating in
61								// place. For now though a partial parse request re-parses it all.
62								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								// if let Some(stylesheet) = &result.output {
68								// 	trace!("Sucessfully parsed stylesheet: {:#?}", &stylesheet);
69								// }
70								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				// notebook_document_sync: (),
205				// selection_range_provider: (),
206				// hover_provider: (),
207				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				// signature_help_provider: (),
215				// definition_provider: (),
216				// type_definition_provider: (),
217				// implementation_provider: (),
218				// references_provider: (),
219				// document_highlight_provider: (),
220				// document_symbol_provider: (),
221				// workspace_symbol_provider: (),
222				// code_action_provider: (),
223				// code_lens_provider: (),
224				// document_formatting_provider: (),
225				// document_range_formatting_provider: (),
226				// document_on_type_formatting_provider: (),
227				// rename_provider: (),
228				// document_link_provider: (),
229				// color_provider: (),
230				// folding_range_provider: (),
231				// declaration_provider: (),
232				// execute_command_provider: (),
233				// workspace: (),
234				// call_hierarchy_provider: (),
235				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				// moniker_provider: (),
255				// linked_editing_range_provider: (),
256				// inline_value_provider: (),
257				// inlay_hint_provider: (),
258				// diagnostic_provider: (),
259				// inline_completion_provider: (),
260				// experimental: (),
261				..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		// let uri = req.text_document_position.text_document.uri;
292		// let position = req.text_document_position.position;
293		// let context = req.context;
294		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}