Skip to main content

csskit_lsp/
service.rs

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