csskit_lsp/
service.rs

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