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 RopeChange(Rope),
29 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 drop(result);
70 bump.reset();
71 string = rope.clone().into();
72 result = Parser::new(&bump, &string).parse_entirely::<StyleSheet>();
73 }
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 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 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 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 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 ..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 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}