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 RopeChange(Rope),
30 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 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 }
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 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 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 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 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 ..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 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}