Skip to content

Commit 99d781b

Browse files
committed
(lsp) use string ropes for incremental edits, scoped per-file threads for parsing
1 parent 71e5f0a commit 99d781b

File tree

6 files changed

+213
-61
lines changed

6 files changed

+213
-61
lines changed

Cargo.lock

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ hdx_lsp = { version = "0.0.0", path = "crates/hdx_lsp" }
2929
bumpalo = { version = "3.16.0" }
3030

3131
# Data structure libraries/helpers
32-
smallvec = { version = "1.13.2" }
32+
bitmask-enum = { version = "2.2.5" }
3333
itertools = { version = "0.13.0" }
34+
ropey = { version = "1.6.1" }
35+
smallvec = { version = "1.13.2" }
3436
strum = { version = "0.26.3" }
35-
bitmask-enum = { version = "2.2.5" }
3637

3738
# CLI
3839
clap = { version = "4.5.23" }

crates/hdx_lsp/Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ hdx_highlight = { workspace = true }
2020
bumpalo = { workspace = true, features = ["collections", "boxed"] }
2121
miette = { workspace = true, features = ["derive"] }
2222

23-
smallvec = { workspace = true }
2423
bitmask-enum = { workspace = true }
25-
strum = { workspace = true, features = ["derive"] }
2624
itertools = { workspace = true }
25+
ropey = { workspace = true }
26+
smallvec = { workspace = true }
27+
strum = { workspace = true, features = ["derive"] }
2728

2829
serde = { workspace = true }
2930
serde_json = { workspace = true }

crates/hdx_lsp/src/server.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ mod tests {
110110

111111
use super::*;
112112
use lsp_types::{
113-
request::{GotoDeclaration, GotoDeclarationParams, Initialize, Request as RequestTrait},
114-
GotoDefinitionResponse, InitializeParams, InitializeResult,
113+
request::{GotoDeclaration, Initialize, Request as RequestTrait},
114+
InitializeParams, InitializeResult,
115115
};
116116
use serde_json::{json, to_value, Value};
117117
use tracing::level_filters::LevelFilter;
@@ -122,7 +122,7 @@ mod tests {
122122
let stderr_log = fmt::layer().with_writer(io::stderr).with_filter(LevelFilter::TRACE);
123123
struct TestHandler {}
124124
impl Handler for TestHandler {
125-
fn initialize(&self, req: InitializeParams) -> Result<InitializeResult, ErrorCode> {
125+
fn initialize(&self, _req: InitializeParams) -> Result<InitializeResult, ErrorCode> {
126126
Ok(InitializeResult { ..Default::default() })
127127
}
128128
}

crates/hdx_lsp/src/server/handler.rs

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ pub trait Handler: Sized + Send + Sync + 'static {
2424
let span = trace_span!("Handling request", "{:#?}", message);
2525
let _ = span.enter();
2626
let id = message.id().unwrap_or_default();
27-
debug!("LspMessageHandler -> {:#?}", &message);
2827
if message.is_exit_notification() {
2928
return None;
3029
}

crates/hdx_lsp/src/service.rs

+187-53
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,135 @@
11
use bumpalo::Bump;
2+
use crossbeam_channel::{bounded, Receiver, Sender};
23
use dashmap::DashMap;
34
use hdx_ast::css::{StyleSheet, Visitable};
4-
use hdx_highlight::{SemanticKind, SemanticModifier, TokenHighlighter};
5-
use hdx_parser::{Features, Parser};
5+
use hdx_highlight::{Highlight, SemanticKind, SemanticModifier, TokenHighlighter};
6+
use hdx_parser::{Features, Parser, ParserReturn};
67
use itertools::Itertools;
78
use lsp_types::Uri;
8-
use std::sync::{
9-
atomic::{AtomicBool, Ordering},
10-
Arc,
9+
use ropey::Rope;
10+
use std::{
11+
sync::{
12+
atomic::{AtomicBool, Ordering},
13+
Arc,
14+
},
15+
thread::{Builder, JoinHandle},
1116
};
1217
use strum::VariantNames;
13-
use tracing::trace;
18+
use tracing::{instrument, trace};
1419

1520
use crate::{ErrorCode, Handler};
1621

22+
type Line = u32;
23+
type Col = u32;
24+
25+
#[derive(Debug)]
26+
enum 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)]
34+
enum FileReturn {
35+
Highlights(Vec<(Highlight, Line, Col)>),
36+
}
37+
38+
#[derive(Debug)]
39+
pub struct File {
40+
pub content: Rope,
41+
thread: JoinHandle<()>,
42+
sender: Sender<FileCall>,
43+
receiver: Receiver<FileReturn>,
44+
}
45+
46+
impl File {
47+
fn new() -> Self {
48+
let (sender, read_receiver) = bounded::<FileCall>(0);
49+
let (write_sender, receiver) = bounded::<FileReturn>(0);
50+
Self {
51+
content: Rope::new(),
52+
sender,
53+
receiver,
54+
thread: Builder::new()
55+
.name("LspDocumentHandler".into())
56+
.spawn(move || {
57+
let mut bump = Bump::default();
58+
let mut string: String = "".into();
59+
let mut result: ParserReturn<'_, StyleSheet<'_>> =
60+
Parser::new(&bump, "", Features::default()).parse_entirely::<StyleSheet>();
61+
while let Ok(call) = read_receiver.recv() {
62+
trace!("String is currently {:?}", string);
63+
match call {
64+
FileCall::RopeChange(rope) => {
65+
trace!("Parsing document");
66+
// TODO! we should be able to optimize this by parsing a subset of the tree and mutating in
67+
// place. For now though a partial parse request re-parses it all.
68+
drop(result);
69+
bump.reset();
70+
string = rope.clone().into();
71+
result =
72+
Parser::new(&bump, &string, Features::default()).parse_entirely::<StyleSheet>();
73+
if let Some(stylesheet) = &result.output {
74+
trace!("Sucessfully parsed stylesheet: {:#?}", &stylesheet);
75+
}
76+
}
77+
FileCall::Highlight => {
78+
trace!("Highlighting document");
79+
let mut highlighter = TokenHighlighter::new();
80+
if let Some(stylesheet) = &result.output {
81+
stylesheet.accept(&mut highlighter);
82+
let mut current_line = 0;
83+
let mut current_start = 0;
84+
let data = highlighter
85+
.highlights()
86+
.sorted_by(|a, b| Ord::cmp(&a.span(), &b.span()))
87+
.map(|h| {
88+
// TODO: figure out a more efficient way to get line/col
89+
let span_contents = h.span().span_contents(&string);
90+
let (line, start) = span_contents.line_and_column();
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 to_string(&self) -> String {
109+
self.content.clone().into()
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+
while let Ok(ret) = self.receiver.recv() {
121+
if let FileReturn::Highlights(highlights) = ret {
122+
return highlights;
123+
}
124+
}
125+
return vec![];
126+
}
127+
}
128+
129+
#[derive(Debug)]
17130
pub struct LSPService {
18131
version: String,
19-
files: Arc<DashMap<Uri, String>>,
132+
files: Arc<DashMap<Uri, File>>,
20133
initialized: AtomicBool,
21134
}
22135

@@ -27,10 +140,12 @@ impl LSPService {
27140
}
28141

29142
impl Handler for LSPService {
143+
#[instrument]
30144
fn initialized(&self) -> bool {
31145
self.initialized.load(Ordering::SeqCst)
32146
}
33147

148+
#[instrument]
34149
fn initialize(&self, req: lsp_types::InitializeParams) -> Result<lsp_types::InitializeResult, ErrorCode> {
35150
self.initialized.swap(true, Ordering::SeqCst);
36151
Ok(lsp_types::InitializeResult {
@@ -112,71 +227,90 @@ impl Handler for LSPService {
112227
})
113228
}
114229

230+
#[instrument]
115231
fn semantic_tokens_full_request(
116232
&self,
117233
req: lsp_types::SemanticTokensParams,
118234
) -> Result<Option<lsp_types::SemanticTokensResult>, ErrorCode> {
119235
let uri = req.text_document.uri;
120-
let allocator = Bump::default();
121-
if let Some(source_text) = self.files.get(&uri) {
122-
trace!("Asked for SemanticTokens");
123-
let result =
124-
Parser::new(&allocator, source_text.as_str(), Features::default()).parse_entirely::<StyleSheet>();
125-
if let Some(stylesheet) = result.output {
126-
trace!("Sucessfully parsed stylesheet: {:#?}", &stylesheet);
127-
let mut highlighter = TokenHighlighter::new();
128-
stylesheet.accept(&mut highlighter);
129-
let mut current_line = 0;
130-
let mut current_start = 0;
131-
let data = highlighter
132-
.highlights()
133-
.sorted_by(|a, b| Ord::cmp(&a.span(), &b.span()))
134-
.map(|highlight| {
135-
let span_contents = highlight.span().span_contents(source_text.as_str());
136-
let (line, start) = span_contents.line_and_column();
137-
let delta_line = line - current_line;
138-
current_line = line;
139-
let delta_start = if delta_line == 0 { start - current_start } else { start };
140-
current_start = start;
141-
lsp_types::SemanticToken {
142-
token_type: highlight.kind().bits() as u32,
143-
token_modifiers_bitset: highlight.modifier().bits() as u32,
144-
delta_line,
145-
delta_start,
146-
length: span_contents.size(),
147-
}
148-
})
149-
.collect();
150-
return Ok(Some(lsp_types::SemanticTokensResult::Tokens(lsp_types::SemanticTokens {
151-
result_id: None,
152-
data,
153-
})));
154-
} else if !result.errors.is_empty() {
155-
trace!("\n\nParse on {:?} failed. Saw error {:?}", &uri, result.errors);
156-
}
236+
trace!("Asked for SemanticTokens for {:?}", &uri);
237+
if let Some(document) = self.files.get(&uri) {
238+
let mut current_line = 0;
239+
let mut current_start = 0;
240+
// TODO: remove this, figure out a more efficient way to get line/col
241+
let str = document.to_string();
242+
let data = document
243+
.get_highlights()
244+
.into_iter()
245+
.map(|(highlight, delta_line, delta_start)| lsp_types::SemanticToken {
246+
token_type: highlight.kind().bits() as u32,
247+
token_modifiers_bitset: highlight.modifier().bits() as u32,
248+
delta_line,
249+
delta_start,
250+
length: highlight.span().size(),
251+
})
252+
.collect();
253+
Ok(Some(lsp_types::SemanticTokensResult::Tokens(lsp_types::SemanticTokens { result_id: None, data })))
254+
} else {
255+
Err(ErrorCode::InternalError)
157256
}
158-
Err(ErrorCode::InternalError)
159257
}
160258

259+
#[instrument]
161260
fn completion(&self, req: lsp_types::CompletionParams) -> Result<Option<lsp_types::CompletionResponse>, ErrorCode> {
162-
// let uri = req.text_document.uri;
163-
// let position = req.text_document_position;
164-
// let context = req.context;
165-
Err(ErrorCode::UnknownErrorCode)
261+
let uri = req.text_document_position.text_document.uri;
262+
let position = req.text_document_position.position;
263+
let context = req.context;
264+
Ok(None)
166265
}
167266

267+
#[instrument]
168268
fn on_did_open_text_document(&self, req: lsp_types::DidOpenTextDocumentParams) {
169269
let uri = req.text_document.uri;
170270
let source_text = req.text_document.text;
171-
self.files.clone().insert(uri, source_text);
271+
let mut doc = File::new();
272+
let mut rope = doc.content.clone();
273+
rope.remove(0..);
274+
rope.insert(0, &source_text);
275+
trace!("comitting new document {:?} {:?}", &uri, rope);
276+
doc.commit(rope);
277+
self.files.clone().insert(uri, doc);
172278
}
173279

280+
#[instrument]
174281
fn on_did_change_text_document(&self, req: lsp_types::DidChangeTextDocumentParams) {
175282
let uri = req.text_document.uri;
176283
let changes = req.content_changes;
177-
if changes.len() == 1 && changes[0].range.is_none() {
178-
let source_text = &changes[0].text;
179-
self.files.clone().insert(uri, source_text.into());
284+
if let Some(mut file) = self.files.clone().get_mut(&uri) {
285+
let mut rope = file.content.clone();
286+
for change in changes {
287+
let range = if let Some(range) = change.range {
288+
rope.try_line_to_char(range.start.line as usize).map_or_else(
289+
|_| (0, None),
290+
|start| {
291+
rope.try_line_to_char(range.end.line as usize).map_or_else(
292+
|_| (start + range.start.character as usize, None),
293+
|end| {
294+
(start + range.start.character as usize, Some(end + range.end.character as usize))
295+
},
296+
)
297+
},
298+
)
299+
} else {
300+
(0, None)
301+
};
302+
match range {
303+
(start, None) => {
304+
rope.try_remove(start..).ok();
305+
rope.try_insert(start, &change.text).ok();
306+
}
307+
(start, Some(end)) => {
308+
rope.try_remove(start..end).ok();
309+
rope.try_insert(start, &change.text).ok();
310+
}
311+
}
312+
}
313+
file.commit(rope)
180314
}
181315
}
182316
}

0 commit comments

Comments
 (0)