From 0156f3ebac6772482297075a05a7046ffb9f04d4 Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Wed, 13 Dec 2023 01:52:35 +0900 Subject: [PATCH] feat(lsp): support incremental file changes This allows for faster updates as the editor can send only changed content instead of the whole document on each modification. --- Cargo.lock | 17 +++++ crates/stef-lsp/Cargo.toml | 1 + crates/stef-lsp/src/compile.rs | 14 +++-- crates/stef-lsp/src/main.rs | 109 ++++++++++++++++++++++++++++++--- 4 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6b92e2..9cb17d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,16 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1054,6 +1064,7 @@ dependencies = [ "lsp-types", "ouroboros", "parking_lot", + "ropey", "serde", "serde_json", "stef-compiler", @@ -1081,6 +1092,12 @@ dependencies = [ "stef-build", ] +[[package]] +name = "str_indices" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" + [[package]] name = "strsim" version = "0.10.0" diff --git a/crates/stef-lsp/Cargo.toml b/crates/stef-lsp/Cargo.toml index 9f18247..5ec9188 100644 --- a/crates/stef-lsp/Cargo.toml +++ b/crates/stef-lsp/Cargo.toml @@ -19,6 +19,7 @@ lsp-server = "0.7.5" lsp-types = { version = "0.94.1", features = ["proposed"] } ouroboros = "0.18.1" parking_lot = "0.12.1" +ropey = "1.6.1" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" stef-compiler = { path = "../stef-compiler" } diff --git a/crates/stef-lsp/src/compile.rs b/crates/stef-lsp/src/compile.rs index dbf1af1..0a1ea29 100644 --- a/crates/stef-lsp/src/compile.rs +++ b/crates/stef-lsp/src/compile.rs @@ -13,14 +13,16 @@ use stef_parser::{ Schema, }; -pub fn compile(file: Url, schema: &str) -> std::result::Result, Diagnostic> { - let index = LineIndex::new(schema); - - let parsed = stef_parser::Schema::parse(schema, None) - .map_err(|e| parse_schema_diagnostic(&index, &e))?; +pub fn compile<'a>( + file: Url, + schema: &'a str, + index: &'_ LineIndex, +) -> std::result::Result, Diagnostic> { + let parsed = + stef_parser::Schema::parse(schema, None).map_err(|e| parse_schema_diagnostic(index, &e))?; stef_compiler::validate_schema(&parsed) - .map_err(|e| validate_schema_diagnostic(file, &index, e))?; + .map_err(|e| validate_schema_diagnostic(file, index, e))?; Ok(parsed) } diff --git a/crates/stef-lsp/src/main.rs b/crates/stef-lsp/src/main.rs index 121c852..8c506f4 100644 --- a/crates/stef-lsp/src/main.rs +++ b/crates/stef-lsp/src/main.rs @@ -4,7 +4,8 @@ use std::{collections::HashMap, net::Ipv4Addr, time::Duration}; use anyhow::{bail, ensure, Context, Result}; -use log::{as_debug, as_display, debug, error, info}; +use line_index::{LineIndex, TextRange}; +use log::{as_debug, as_display, debug, error, info, warn}; use lsp_server::{Connection, Message, Notification, Request, RequestId, Response}; use lsp_types::{ notification::{ @@ -25,6 +26,7 @@ use lsp_types::{ TextDocumentSyncKind, Url, WorkDoneProgressOptions, }; use ouroboros::self_referencing; +use ropey::Rope; use stef_parser::Schema; use self::cli::Cli; @@ -44,8 +46,10 @@ struct Backend { #[self_referencing] #[derive(Debug)] struct File { + rope: Rope, + index: LineIndex, content: String, - #[borrows(content)] + #[borrows(index, content)] #[covariant] schema: Result, Diagnostic>, } @@ -163,7 +167,7 @@ impl LanguageServer for Backend { capabilities: ServerCapabilities { position_encoding: Some(PositionEncodingKind::UTF16), text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::FULL, + TextDocumentSyncKind::INCREMENTAL, )), semantic_tokens_provider: Some( SemanticTokensServerCapabilities::SemanticTokensOptions( @@ -245,9 +249,14 @@ impl LanguageServer for Backend { fn did_open(&mut self, params: DidOpenTextDocumentParams) { debug!(uri = as_display!(params.text_document.uri); "schema opened"); + let text = params.text_document.text; let file = FileBuilder { - content: params.text_document.text, - schema_builder: |schema| compile::compile(params.text_document.uri.clone(), schema), + rope: Rope::from_str(&text), + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, } .build(); @@ -269,11 +278,57 @@ impl LanguageServer for Backend { fn did_change(&mut self, mut params: DidChangeTextDocumentParams) { debug!(uri = as_display!(params.text_document.uri); "schema changed"); - let file = FileBuilder { - content: params.content_changes.remove(0).text, - schema_builder: |schema| compile::compile(params.text_document.uri.clone(), schema), - } - .build(); + let file = if params.content_changes.len() == 1 + && params + .content_changes + .first() + .is_some_and(|change| change.range.is_none()) + { + let text = params.content_changes.remove(0).text; + FileBuilder { + rope: Rope::from_str(&text), + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, + } + .build() + } else { + let Some(file) = self.files.remove(¶ms.text_document.uri) else { + warn!("missing state for changed file"); + return; + }; + + let mut heads = file.into_heads(); + + for change in params.content_changes { + let range = match convert_range(&heads.index, change.range) { + Ok(range) => range, + Err(e) => { + error!(error = as_debug!(e); "invalid change"); + continue; + } + }; + + let start = heads.rope.byte_to_char(range.start().into()); + let end = heads.rope.byte_to_char(range.end().into()); + heads.rope.remove(start..end); + heads.rope.insert(start, &change.text); + } + + let text = String::from(&heads.rope); + + FileBuilder { + rope: heads.rope, + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, + } + .build() + }; if let Err(e) = self.publish_diagnostics( params.text_document.uri.clone(), @@ -429,3 +484,37 @@ where { notif.extract(R::METHOD).map_err(Into::into) } + +fn convert_range(index: &LineIndex, range: Option) -> Result { + let range = range.context("incremental change misses range")?; + + let start = index + .offset( + index + .to_utf8( + line_index::WideEncoding::Utf16, + line_index::WideLineCol { + line: range.start.line, + col: range.start.character, + }, + ) + .context("failed to convert start position to utf-8")?, + ) + .context("failed to convert start position to byte offset")?; + + let end = index + .offset( + index + .to_utf8( + line_index::WideEncoding::Utf16, + line_index::WideLineCol { + line: range.end.line, + col: range.end.character, + }, + ) + .context("failed to convert end position to utf-8")?, + ) + .context("failed to convert end position to byte offset")?; + + Ok(TextRange::new(start, end)) +}