use crate::{
ast::Node,
error::ParseResult,
incremental_document::{IncrementalDocument, ParseMetrics},
incremental_edit::{IncrementalEdit, IncrementalEditSet},
parser::Parser,
};
use ropey::Rope;
use serde_json::Value;
use std::sync::Arc;
pub struct IncrementalConfig {
pub enabled: bool,
pub target_parse_time_ms: f64,
pub max_cache_size: usize,
}
impl Default for IncrementalConfig {
fn default() -> Self {
Self {
enabled: std::env::var("PERL_LSP_INCREMENTAL").is_ok(),
target_parse_time_ms: 1.0,
max_cache_size: 10000,
}
}
}
pub fn lsp_change_to_edit(change: &Value, rope: &Rope) -> Option<IncrementalEdit> {
if let Some(range) = change.get("range") {
let start_line = range["start"]["line"].as_u64()? as usize;
let start_char = range["start"]["character"].as_u64()? as usize;
let end_line = range["end"]["line"].as_u64()? as usize;
let end_char = range["end"]["character"].as_u64()? as usize;
let start_byte = lsp_pos_to_byte(rope, start_line, start_char);
let end_byte = lsp_pos_to_byte(rope, end_line, end_char);
let new_text = change["text"].as_str()?.to_string();
let start_position =
crate::position::Position::new(start_byte, start_line as u32, start_char as u32);
let old_end_position =
crate::position::Position::new(end_byte, end_line as u32, end_char as u32);
Some(IncrementalEdit::with_positions(
start_byte,
end_byte,
new_text,
start_position,
old_end_position,
))
} else {
None
}
}
pub fn lsp_pos_to_byte(rope: &Rope, line: usize, character: usize) -> usize {
if line >= rope.len_lines() {
return rope.len_bytes();
}
let line_start = rope.line_to_byte(line);
let line = rope.line(line);
let mut utf16_pos = 0;
let mut byte_pos = 0;
for ch in line.chars() {
if utf16_pos >= character {
break;
}
utf16_pos += ch.len_utf16();
byte_pos += ch.len_utf8();
}
line_start + byte_pos
}
pub fn byte_to_lsp_pos(rope: &Rope, byte_offset: usize) -> (usize, usize) {
let byte_offset = byte_offset.min(rope.len_bytes());
let line = rope.byte_to_line(byte_offset);
let line_start = rope.line_to_byte(line);
let column_bytes = byte_offset - line_start;
let line_str = rope.line(line);
let mut utf16_pos = 0;
let mut current_bytes = 0;
for ch in line_str.chars() {
if current_bytes >= column_bytes {
break;
}
current_bytes += ch.len_utf8();
utf16_pos += ch.len_utf16();
}
(line, utf16_pos)
}
pub enum DocumentParser {
Full { content: String, ast: Option<Arc<Node>> },
Incremental { document: Box<IncrementalDocument>, rope: Rope },
}
impl DocumentParser {
pub fn new(content: String, config: &IncrementalConfig) -> ParseResult<Self> {
if config.enabled {
let document = IncrementalDocument::new(content.clone())?;
let rope = Rope::from_str(&content);
Ok(DocumentParser::Incremental { document: Box::new(document), rope })
} else {
let mut parser = Parser::new(&content);
let ast = parser.parse().ok().map(Arc::new);
Ok(DocumentParser::Full { content, ast })
}
}
pub fn apply_changes(
&mut self,
changes: &[Value],
_config: &IncrementalConfig,
) -> ParseResult<()> {
match self {
DocumentParser::Full { content, ast } => {
if let Some(change) = changes.first() {
if let Some(text) = change["text"].as_str() {
*content = text.to_string();
let mut parser = Parser::new(content);
*ast = parser.parse().ok().map(Arc::new);
}
}
Ok(())
}
DocumentParser::Incremental { document: boxed_document, rope } => {
let document = boxed_document.as_mut();
let mut edits = Vec::new();
for change in changes {
if let Some(edit) = lsp_change_to_edit(change, rope) {
edits.push(edit);
} else {
if let Some(text) = change["text"].as_str() {
*document = IncrementalDocument::new(text.to_string())?;
*rope = Rope::from_str(text);
return Ok(());
}
}
}
if !edits.is_empty() {
let edit_set = IncrementalEditSet { edits };
document.apply_edits(&edit_set)?;
*rope = Rope::from_str(&document.source);
}
Ok(())
}
}
}
pub fn ast(&self) -> Option<Arc<Node>> {
match self {
DocumentParser::Full { ast, .. } => ast.clone(),
DocumentParser::Incremental { document, .. } => Some(document.root.clone()),
}
}
pub fn content(&self) -> &str {
match self {
DocumentParser::Full { content, .. } => content,
DocumentParser::Incremental { document, .. } => &document.source,
}
}
pub fn metrics(&self) -> Option<&ParseMetrics> {
match self {
DocumentParser::Full { .. } => None,
DocumentParser::Incremental { document, .. } => Some(document.metrics()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lsp_pos_to_byte() {
let text = "Hello\nWorld\n";
let rope = Rope::from_str(text);
assert_eq!(lsp_pos_to_byte(&rope, 0, 0), 0);
assert_eq!(lsp_pos_to_byte(&rope, 1, 0), 6);
assert_eq!(lsp_pos_to_byte(&rope, 1, 3), 9);
}
#[test]
fn test_byte_to_lsp_pos() {
let text = "Hello\nWorld\n";
let rope = Rope::from_str(text);
assert_eq!(byte_to_lsp_pos(&rope, 0), (0, 0));
assert_eq!(byte_to_lsp_pos(&rope, 6), (1, 0));
assert_eq!(byte_to_lsp_pos(&rope, 9), (1, 3));
}
#[test]
fn test_crlf_handling() {
let text = "Hello\r\nWorld\r\n";
let rope = Rope::from_str(text);
assert_eq!(lsp_pos_to_byte(&rope, 1, 0), 7);
assert_eq!(byte_to_lsp_pos(&rope, 7), (1, 0));
}
#[test]
fn test_utf16_handling() {
let text = "Hello 😀 World"; let rope = Rope::from_str(text);
let byte_after_emoji = "Hello 😀".len();
let (line, char) = byte_to_lsp_pos(&rope, byte_after_emoji);
assert_eq!(line, 0);
assert_eq!(char, 8); }
}