Skip to content
This repository was archived by the owner on Jan 25, 2024. It is now read-only.

Commit e1ec7a0

Browse files
authored
Enable incremental parsing via the LSP protocol (#37)
* Enable incremental parsing via the LSP protocol With incremental parsing, the editor only sends a "diff" of what has changed rather than the full file. For instance, it's pretty pointless to send all of `all-packages.nix` from `nixpkgs` for a single-letter change. Given the benchmark of `rnix-parser`[1] it doesn't seem as the parsing itself is the bottleneck a few people have reported[2], so this change only replaces affected lines and re-parses everything using `rnix-parser`. While the current change seems usable on local tests, it has (at least) two known issues: * As soon as you type `inherit`, the LSP stalls and it seems it's due to `rnix-parser` and steadily allocates more memory according to `strace(1)`. I confirmed that it's indeed a problem of `rnix-parser` by creating a file with `{ inherit` as content and the LSP refuses to respond (with incremental parsing disabled). * Given an expression like this: ``` nix let a = 1; in { b = a; } ``` When deleting line one and then line 2, the state ends up as `{{ }`. This is because the range passed to LSP looks like this: ``` Range { start: Position { line: 1, character: 0 }, end: Position { line: 2, character: 0 } } ``` This also kills the second line of the expression above and as soon as you delete this as well, the state will end up with bogus results. It's a bit weird though since I got an equal message when deleting more lines, so I'm not entirely sure if that's just a bug in the LSP implementation of my neoVIM (and also the reason why I didn't just insert a `\n` at the right vec entries). [1] https://github.com/nix-community/rnix-parser/blob/v0.9.0/benches/all-packages.rs [2] #27 (comment) * Don't swallow empty lines if the previous one (including the `\n` separator) gets deleted * Simplify (& hopefully fix) incremental parsing Rather than trying to mess around in an existing datastructure and thus running into tons of mean special cases since you have to take care of removing/adding lines on your own, we just write everything to a fresh string and use this for parsing and inside the HashMap containing the AST of each known Nix expression. Not sure if it works out the way I hope, but it seems a little bit better now. Will test it a while on my setup before I consider this ready. * Fix newlines
1 parent 753c379 commit e1ec7a0

File tree

1 file changed

+54
-4
lines changed

1 file changed

+54
-4
lines changed

src/main.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use rnix::{
3939
SyntaxNode, TextRange, TextSize,
4040
};
4141
use std::{
42+
borrow::Cow,
4243
collections::HashMap,
4344
panic,
4445
path::{Path, PathBuf},
@@ -68,7 +69,7 @@ fn real_main() -> Result<(), Error> {
6869
text_document_sync: Some(TextDocumentSyncCapability::Options(
6970
TextDocumentSyncOptions {
7071
open_close: Some(true),
71-
change: Some(TextDocumentSyncKind::Full),
72+
change: Some(TextDocumentSyncKind::Incremental),
7273
..TextDocumentSyncOptions::default()
7374
},
7475
)),
@@ -232,10 +233,59 @@ impl App {
232233
DidChangeTextDocument::METHOD => {
233234
let params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?;
234235
if let Some(change) = params.content_changes.into_iter().last() {
235-
let parsed = rnix::parse(&change.text);
236-
self.send_diagnostics(params.text_document.uri.clone(), &change.text, &parsed)?;
236+
let uri = params.text_document.uri;
237+
let mut content = Cow::from(&change.text);
238+
if let Some(range) = &change.range {
239+
if self.files.contains_key(&uri) {
240+
let original = self.files.get(&uri)
241+
.unwrap().1.lines().collect::<Vec<_>>();
242+
let start_line = range.start.line;
243+
let start_char = range.start.character;
244+
let end_line = range.end.line;
245+
let end_char = range.end.character;
246+
247+
let mut out = String::from("");
248+
let len = original.len() as u64;
249+
for i in 0..len {
250+
if i < start_line || i > end_line {
251+
out += original.get(i as usize).unwrap();
252+
if i != len - 1 {
253+
out += "\n";
254+
}
255+
continue;
256+
}
257+
if i == start_line {
258+
out += &original
259+
.get(i as usize)
260+
.unwrap()
261+
.chars()
262+
.into_iter()
263+
.take(start_char as usize)
264+
.collect::<String>();
265+
out += &change.text;
266+
}
267+
if i == end_line {
268+
out += &original
269+
.get(i as usize)
270+
.unwrap()
271+
.chars()
272+
.into_iter()
273+
.skip(end_char as usize)
274+
.collect::<String>();
275+
276+
if i != len - 1 {
277+
out += "\n";
278+
}
279+
}
280+
}
281+
282+
content = Cow::Owned(out);
283+
}
284+
}
285+
let parsed = rnix::parse(&content);
286+
self.send_diagnostics(uri.clone(), &content, &parsed)?;
237287
self.files
238-
.insert(params.text_document.uri, (parsed, change.text));
288+
.insert(uri, (parsed, content.to_owned().to_string()));
239289
}
240290
}
241291
_ => (),

0 commit comments

Comments
 (0)