Skip to content

Commit

Permalink
feat(lsp): support incremental file changes
Browse files Browse the repository at this point in the history
This allows for faster updates as the editor can send only changed
content instead of the whole document on each modification.
  • Loading branch information
dnaka91 committed Dec 12, 2023
1 parent dc61f6e commit 0156f3e
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 16 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/stef-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
14 changes: 8 additions & 6 deletions crates/stef-lsp/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ use stef_parser::{
Schema,
};

pub fn compile(file: Url, schema: &str) -> std::result::Result<Schema<'_>, 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<Schema<'a>, 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)
}
Expand Down
109 changes: 99 additions & 10 deletions crates/stef-lsp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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;
Expand All @@ -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<Schema<'this>, Diagnostic>,
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();

Expand All @@ -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(&params.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(),
Expand Down Expand Up @@ -429,3 +484,37 @@ where
{
notif.extract(R::METHOD).map_err(Into::into)
}

fn convert_range(index: &LineIndex, range: Option<lsp_types::Range>) -> Result<TextRange> {
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))
}

0 comments on commit 0156f3e

Please sign in to comment.