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 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 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 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 }
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 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 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 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 ..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 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}