csskit_lsp/
service.rs

1use bumpalo::Bump;
2use crossbeam_channel::{Receiver, Sender, bounded};
3use css_ast::{CssAtomSet, StyleSheet, Visitable};
4use css_lexer::Lexer;
5use css_parse::{Parser, ParserReturn};
6use csskit_highlight::{Highlight, SemanticKind, SemanticModifier, TokenHighlighter};
7use dashmap::DashMap;
8use itertools::Itertools;
9use lsp_types::Uri;
10use ropey::Rope;
11use std::{
12	sync::{
13		Arc,
14		atomic::{AtomicBool, Ordering},
15	},
16	thread::{Builder, JoinHandle},
17};
18use strum::VariantNames;
19use tracing::{instrument, trace, trace_span};
20
21use crate::{ErrorCode, Handler};
22
23type Line = u32;
24type Col = u32;
25
26#[derive(Debug)]
27enum FileCall {
28	// Re-parse the document based on changes
29	RopeChange(Rope),
30	// Highlight a document, returning the semantic highlights
31	Highlight,
32}
33
34#[derive(Debug)]
35enum FileReturn {
36	Highlights(Vec<(Highlight, Line, Col)>),
37}
38
39#[derive(Debug)]
40pub struct File {
41	pub content: Rope,
42	#[allow(dead_code)]
43	thread: JoinHandle<()>,
44	sender: Sender<FileCall>,
45	receiver: Receiver<FileReturn>,
46}
47
48impl File {
49	fn new() -> Self {
50		let (sender, read_receiver) = bounded::<FileCall>(0);
51		let (write_sender, receiver) = bounded::<FileReturn>(0);
52		Self {
53			content: Rope::new(),
54			sender,
55			receiver,
56			thread: Builder::new()
57				.name("LspDocumentHandler".into())
58				.spawn(move || {
59					let mut bump = Bump::default();
60					let mut string: String = "".into();
61					let lexer = Lexer::new(&CssAtomSet::ATOMS, "");
62					let mut result: ParserReturn<'_, StyleSheet<'_>> =
63						Parser::new(&bump, "", lexer).parse_entirely::<StyleSheet>();
64					while let Ok(call) = read_receiver.recv() {
65						match call {
66							FileCall::RopeChange(rope) => {
67								let span = trace_span!("Parsing document");
68								let _ = span.enter();
69								// TODO! we should be able to optimize this by parsing a subset of the tree and mutating in
70								// place. For now though a partial parse request re-parses it all.
71								drop(result);
72								bump.reset();
73								string = rope.clone().into();
74								let lexer = Lexer::new(&CssAtomSet::ATOMS, &string);
75								result = Parser::new(&bump, &string, lexer).parse_entirely::<StyleSheet>();
76								// if let Some(stylesheet) = &result.output {
77								// 	trace!("Sucessfully parsed stylesheet: {:#?}", &stylesheet);
78								// }
79							}
80							FileCall::Highlight => {
81								let span = trace_span!("Highlighting document");
82								let _ = span.enter();
83								let mut highlighter = TokenHighlighter::new();
84								if let Some(stylesheet) = &result.output {
85									stylesheet.accept(&mut highlighter);
86									let mut current_line = 0;
87									let mut current_start = 0;
88									let data = highlighter
89										.highlights()
90										.sorted_by(|a, b| Ord::cmp(&a.span(), &b.span()))
91										.map(|h| {
92											// TODO: figure out a more efficient way to get line/col
93											let (line, start) = h.span().line_and_column(&string);
94											let delta_line: Line = line - current_line;
95											current_line = line;
96											let delta_start: Col =
97												if delta_line == 0 { start - current_start } else { start };
98											current_start = start;
99											(*h, delta_line, delta_start)
100										});
101									write_sender.send(FileReturn::Highlights(data.collect())).ok();
102								}
103							}
104						}
105					}
106				})
107				.expect("Failed to document thread Reader"),
108		}
109	}
110
111	fn commit(&mut self, rope: Rope) {
112		self.content = rope;
113		self.sender.send(FileCall::RopeChange(self.content.clone())).unwrap();
114	}
115
116	#[instrument]
117	fn get_highlights(&self) -> Vec<(Highlight, Line, Col)> {
118		self.sender.send(FileCall::Highlight).unwrap();
119		if let Ok(ret) = self.receiver.recv() {
120			let FileReturn::Highlights(highlights) = ret;
121			return highlights;
122		}
123		vec![]
124	}
125}
126
127#[derive(Debug)]
128pub struct LSPService {
129	version: String,
130	files: Arc<DashMap<Uri, File>>,
131	initialized: AtomicBool,
132}
133
134impl LSPService {
135	pub fn new(version: &'static str) -> Self {
136		Self { version: version.into(), files: Arc::new(DashMap::new()), initialized: AtomicBool::new(false) }
137	}
138}
139
140impl Handler for LSPService {
141	#[instrument]
142	fn initialized(&self) -> bool {
143		self.initialized.load(Ordering::SeqCst)
144	}
145
146	#[instrument]
147	fn initialize(&self, req: lsp_types::InitializeParams) -> Result<lsp_types::InitializeResult, ErrorCode> {
148		self.initialized.swap(true, Ordering::SeqCst);
149		Ok(lsp_types::InitializeResult {
150			capabilities: lsp_types::ServerCapabilities {
151				// position_encoding: (),
152				text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
153					lsp_types::TextDocumentSyncOptions {
154						open_close: Some(true),
155						change: Some(lsp_types::TextDocumentSyncKind::INCREMENTAL),
156						will_save: Some(true),
157						will_save_wait_until: Some(false),
158						save: Some(lsp_types::TextDocumentSyncSaveOptions::Supported(false)),
159					},
160				)),
161				// notebook_document_sync: (),
162				// selection_range_provider: (),
163				// hover_provider: (),
164				completion_provider: Some(lsp_types::CompletionOptions {
165					resolve_provider: None,
166					trigger_characters: Some(vec![".".into(), ":".into(), "@".into(), "#".into(), "-".into()]),
167					all_commit_characters: None,
168					work_done_progress_options: lsp_types::WorkDoneProgressOptions { work_done_progress: None },
169					completion_item: None,
170				}),
171				// signature_help_provider: (),
172				// definition_provider: (),
173				// type_definition_provider: (),
174				// implementation_provider: (),
175				// references_provider: (),
176				// document_highlight_provider: (),
177				// document_symbol_provider: (),
178				// workspace_symbol_provider: (),
179				// code_action_provider: (),
180				// code_lens_provider: (),
181				// document_formatting_provider: (),
182				// document_range_formatting_provider: (),
183				// document_on_type_formatting_provider: (),
184				// rename_provider: (),
185				// document_link_provider: (),
186				// color_provider: (),
187				// folding_range_provider: (),
188				// declaration_provider: (),
189				// execute_command_provider: (),
190				// workspace: (),
191				// call_hierarchy_provider: (),
192				semantic_tokens_provider: Some(lsp_types::SemanticTokensServerCapabilities::SemanticTokensOptions(
193					lsp_types::SemanticTokensOptions {
194						work_done_progress_options: lsp_types::WorkDoneProgressOptions {
195							work_done_progress: Some(false),
196						},
197						legend: lsp_types::SemanticTokensLegend {
198							token_types: SemanticKind::VARIANTS
199								.iter()
200								.map(|v| lsp_types::SemanticTokenType::new(v))
201								.collect(),
202							token_modifiers: SemanticModifier::VARIANTS
203								.iter()
204								.map(|v| lsp_types::SemanticTokenModifier::new(v))
205								.collect(),
206						},
207						range: Some(false),
208						full: Some(lsp_types::SemanticTokensFullOptions::Delta { delta: Some(true) }),
209					},
210				)),
211				// moniker_provider: (),
212				// linked_editing_range_provider: (),
213				// inline_value_provider: (),
214				// inlay_hint_provider: (),
215				// diagnostic_provider: (),
216				// inline_completion_provider: (),
217				// experimental: (),
218				..Default::default()
219			},
220			server_info: Some(lsp_types::ServerInfo {
221				name: String::from("csskit-lsp"),
222				version: Some(self.version.clone()),
223			}),
224			offset_encoding: None,
225		})
226	}
227
228	#[instrument]
229	fn semantic_tokens_full_request(
230		&self,
231		req: lsp_types::SemanticTokensParams,
232	) -> Result<Option<lsp_types::SemanticTokensResult>, ErrorCode> {
233		let uri = req.text_document.uri;
234		trace!("Asked for SemanticTokens for {:?}", &uri);
235		if let Some(document) = self.files.get(&uri) {
236			let data = document
237				.get_highlights()
238				.into_iter()
239				.map(|(highlight, delta_line, delta_start)| lsp_types::SemanticToken {
240					token_type: highlight.kind().bits() as u32,
241					token_modifiers_bitset: highlight.modifier().bits() as u32,
242					delta_line,
243					delta_start,
244					length: highlight.span().len(),
245				})
246				.collect();
247			Ok(Some(lsp_types::SemanticTokensResult::Tokens(lsp_types::SemanticTokens { result_id: None, data })))
248		} else {
249			Err(ErrorCode::InternalError)
250		}
251	}
252
253	#[instrument]
254	fn completion(&self, req: lsp_types::CompletionParams) -> Result<Option<lsp_types::CompletionResponse>, ErrorCode> {
255		// let uri = req.text_document_position.text_document.uri;
256		// let position = req.text_document_position.position;
257		// let context = req.context;
258		Ok(None)
259	}
260
261	#[instrument]
262	fn on_did_open_text_document(&self, req: lsp_types::DidOpenTextDocumentParams) {
263		let uri = req.text_document.uri;
264		let source_text = req.text_document.text;
265		let mut doc = File::new();
266		let mut rope = doc.content.clone();
267		rope.remove(0..);
268		rope.insert(0, &source_text);
269		trace!("comitting new document {:?} {:?}", &uri, rope);
270		doc.commit(rope);
271		self.files.clone().insert(uri, doc);
272	}
273
274	#[instrument]
275	fn on_did_change_text_document(&self, req: lsp_types::DidChangeTextDocumentParams) {
276		let uri = req.text_document.uri;
277		let changes = req.content_changes;
278		if let Some(mut file) = self.files.clone().get_mut(&uri) {
279			let mut rope = file.content.clone();
280			for change in changes {
281				let range = if let Some(range) = change.range {
282					rope.try_line_to_char(range.start.line as usize).map_or_else(
283						|_| (0, None),
284						|start| {
285							rope.try_line_to_char(range.end.line as usize).map_or_else(
286								|_| (start + range.start.character as usize, None),
287								|end| {
288									(start + range.start.character as usize, Some(end + range.end.character as usize))
289								},
290							)
291						},
292					)
293				} else {
294					(0, None)
295				};
296				match range {
297					(start, None) => {
298						rope.try_remove(start..).ok();
299						rope.try_insert(start, &change.text).ok();
300					}
301					(start, Some(end)) => {
302						rope.try_remove(start..end).ok();
303						rope.try_insert(start, &change.text).ok();
304					}
305				}
306			}
307			file.commit(rope)
308		}
309	}
310}