From 1799355e279d870b6620e2ad759df24800c81cdf Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Mon, 9 Dec 2024 23:34:55 -0600 Subject: [PATCH] add document store and support for didopen, didchange, and didclose (#11) --- crates/djls/src/documents.rs | 250 +++++++++++++++++++++++++++++++++++ crates/djls/src/main.rs | 53 +++++++- crates/djls/src/server.rs | 47 ++++++- 3 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 crates/djls/src/documents.rs diff --git a/crates/djls/src/documents.rs b/crates/djls/src/documents.rs new file mode 100644 index 0000000..9e4738a --- /dev/null +++ b/crates/djls/src/documents.rs @@ -0,0 +1,250 @@ +use anyhow::{anyhow, Result}; +use std::collections::HashMap; +use tower_lsp::lsp_types::{ + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Position, + Range, +}; + +#[derive(Debug)] +pub struct Store { + documents: HashMap, + versions: HashMap, +} + +impl Store { + pub fn new() -> Self { + Self { + documents: HashMap::new(), + versions: HashMap::new(), + } + } + + pub fn handle_did_open(&mut self, params: DidOpenTextDocumentParams) -> Result<()> { + let document = TextDocument::new( + String::from(params.text_document.uri), + params.text_document.text, + params.text_document.version, + params.text_document.language_id, + ); + + self.add_document(document); + + Ok(()) + } + + pub fn handle_did_change(&mut self, params: DidChangeTextDocumentParams) -> Result<()> { + let uri = params.text_document.uri.as_str().to_string(); + let version = params.text_document.version; + + let document = self + .get_document_mut(&uri) + .ok_or_else(|| anyhow!("Document not found: {}", uri))?; + + for change in params.content_changes { + if let Some(range) = change.range { + document.apply_change(range, &change.text)?; + } else { + // Full document update + document.set_content(change.text); + } + } + + document.version = version; + self.versions.insert(uri, version); + + Ok(()) + } + + pub fn handle_did_close(&mut self, params: DidCloseTextDocumentParams) -> Result<()> { + self.remove_document(&String::from(params.text_document.uri)); + + Ok(()) + } + + fn add_document(&mut self, document: TextDocument) { + let uri = document.uri.clone(); + let version = document.version; + + self.documents.insert(uri.clone(), document); + self.versions.insert(uri, version); + } + + fn remove_document(&mut self, uri: &str) { + self.documents.remove(uri); + self.versions.remove(uri); + } + + fn get_document(&self, uri: &str) -> Option<&TextDocument> { + self.documents.get(uri) + } + + fn get_document_mut(&mut self, uri: &str) -> Option<&mut TextDocument> { + self.documents.get_mut(uri) + } + + pub fn get_all_documents(&self) -> impl Iterator { + self.documents.values() + } + + pub fn get_documents_by_language( + &self, + language_id: LanguageId, + ) -> impl Iterator { + self.documents + .values() + .filter(move |doc| doc.language_id == language_id) + } + + pub fn get_version(&self, uri: &str) -> Option { + self.versions.get(uri).copied() + } + + pub fn is_version_valid(&self, uri: &str, version: i32) -> bool { + self.get_version(uri).map_or(false, |v| v == version) + } +} + +#[derive(Clone, Debug)] +pub struct TextDocument { + uri: String, + contents: String, + index: LineIndex, + version: i32, + language_id: LanguageId, +} + +impl TextDocument { + fn new(uri: String, contents: String, version: i32, language_id: String) -> Self { + let index = LineIndex::new(&contents); + Self { + uri, + contents, + index, + version, + language_id: LanguageId::from(language_id), + } + } + + pub fn apply_change(&mut self, range: Range, new_text: &str) -> Result<()> { + let start_offset = self + .index + .offset(range.start) + .ok_or_else(|| anyhow!("Invalid start position: {:?}", range.start))? + as usize; + let end_offset = self + .index + .offset(range.end) + .ok_or_else(|| anyhow!("Invalid end position: {:?}", range.end))? + as usize; + + let mut new_content = String::with_capacity( + self.contents.len() - (end_offset - start_offset) + new_text.len(), + ); + + new_content.push_str(&self.contents[..start_offset]); + new_content.push_str(new_text); + new_content.push_str(&self.contents[end_offset..]); + + self.set_content(new_content); + + Ok(()) + } + + pub fn set_content(&mut self, new_content: String) { + self.contents = new_content; + self.index = LineIndex::new(&self.contents); + } + + pub fn get_text(&self) -> &str { + &self.contents + } + + pub fn get_text_range(&self, range: Range) -> Option<&str> { + let start = self.index.offset(range.start)? as usize; + let end = self.index.offset(range.end)? as usize; + + Some(&self.contents[start..end]) + } + + pub fn get_line(&self, line: u32) -> Option<&str> { + let start = self.index.line_starts.get(line as usize)?; + let end = self + .index + .line_starts + .get(line as usize + 1) + .copied() + .unwrap_or(self.index.length); + + Some(&self.contents[*start as usize..end as usize]) + } + + pub fn line_count(&self) -> usize { + self.index.line_starts.len() + } +} + +#[derive(Clone, Debug)] +pub struct LineIndex { + line_starts: Vec, + length: u32, +} + +impl LineIndex { + pub fn new(text: &str) -> Self { + let mut line_starts = vec![0]; + let mut pos = 0; + + for c in text.chars() { + pos += c.len_utf8() as u32; + if c == '\n' { + line_starts.push(pos); + } + } + + Self { + line_starts, + length: pos, + } + } + + pub fn offset(&self, position: Position) -> Option { + let line_start = self.line_starts.get(position.line as usize)?; + + Some(line_start + position.character) + } + + pub fn position(&self, offset: u32) -> Position { + let line = match self.line_starts.binary_search(&offset) { + Ok(line) => line, + Err(line) => line - 1, + }; + + let line_start = self.line_starts[line]; + let character = offset - line_start; + + Position::new(line as u32, character) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum LanguageId { + HtmlDjango, + Other, + Python, +} + +impl From<&str> for LanguageId { + fn from(language_id: &str) -> Self { + match language_id { + "htmldjango" => Self::HtmlDjango, + "python" => Self::Python, + _ => Self::Other, + } + } +} + +impl From for LanguageId { + fn from(language_id: String) -> Self { + Self::from(language_id.as_str()) + } +} diff --git a/crates/djls/src/main.rs b/crates/djls/src/main.rs index 40851b4..76172c2 100644 --- a/crates/djls/src/main.rs +++ b/crates/djls/src/main.rs @@ -1,3 +1,4 @@ +mod documents; mod notifier; mod server; @@ -5,37 +6,77 @@ use crate::notifier::TowerLspNotifier; use crate::server::{DjangoLanguageServer, LspNotification, LspRequest}; use anyhow::Result; use djls_django::DjangoProject; +use std::sync::Arc; +use tokio::sync::RwLock; use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types::*; use tower_lsp::{LanguageServer, LspService, Server}; struct TowerLspBackend { - server: DjangoLanguageServer, + server: Arc>, } #[tower_lsp::async_trait] impl LanguageServer for TowerLspBackend { async fn initialize(&self, params: InitializeParams) -> LspResult { self.server + .read() + .await .handle_request(LspRequest::Initialize(params)) .map_err(|_| tower_lsp::jsonrpc::Error::internal_error()) } async fn initialized(&self, params: InitializedParams) { - if self + if let Err(e) = self .server + .write() + .await .handle_notification(LspNotification::Initialized(params)) - .is_err() { - // Handle error + eprintln!("Error handling initialized: {}", e); } } async fn shutdown(&self) -> LspResult<()> { self.server + .write() + .await .handle_notification(LspNotification::Shutdown) .map_err(|_| tower_lsp::jsonrpc::Error::internal_error()) } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + if let Err(e) = self + .server + .write() + .await + .handle_notification(LspNotification::DidOpenTextDocument(params)) + { + eprintln!("Error handling document open: {}", e); + } + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + if let Err(e) = self + .server + .write() + .await + .handle_notification(LspNotification::DidChangeTextDocument(params)) + { + eprintln!("Error handling document change: {}", e); + } + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + if let Err(e) = self + .server + .write() + .await + .handle_notification(LspNotification::DidCloseTextDocument(params)) + { + eprintln!("Error handling document close: {}", e); + } + } } #[tokio::main] @@ -48,7 +89,9 @@ async fn main() -> Result<()> { let (service, socket) = LspService::build(|client| { let notifier = Box::new(TowerLspNotifier::new(client.clone())); let server = DjangoLanguageServer::new(django, notifier); - TowerLspBackend { server } + TowerLspBackend { + server: Arc::new(RwLock::new(server)), + } }) .finish(); diff --git a/crates/djls/src/server.rs b/crates/djls/src/server.rs index 7f1a9a4..d7f9a14 100644 --- a/crates/djls/src/server.rs +++ b/crates/djls/src/server.rs @@ -1,3 +1,4 @@ +use crate::documents::Store; use crate::notifier::Notifier; use anyhow::Result; use djls_django::DjangoProject; @@ -11,6 +12,9 @@ pub enum LspRequest { } pub enum LspNotification { + DidOpenTextDocument(DidOpenTextDocumentParams), + DidChangeTextDocument(DidChangeTextDocumentParams), + DidCloseTextDocument(DidCloseTextDocumentParams), Initialized(InitializedParams), Shutdown, } @@ -18,19 +22,30 @@ pub enum LspNotification { pub struct DjangoLanguageServer { django: DjangoProject, notifier: Box, + documents: Store, } impl DjangoLanguageServer { pub fn new(django: DjangoProject, notifier: Box) -> Self { - Self { django, notifier } + Self { + django, + notifier, + documents: Store::new(), + } } pub fn handle_request(&self, request: LspRequest) -> Result { match request { LspRequest::Initialize(_params) => Ok(InitializeResult { capabilities: ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: Some(false), + will_save_wait_until: Some(false), + save: Some(SaveOptions::default().into()), + }, )), ..Default::default() }, @@ -43,8 +58,32 @@ impl DjangoLanguageServer { } } - pub fn handle_notification(&self, notification: LspNotification) -> Result<()> { + pub fn handle_notification(&mut self, notification: LspNotification) -> Result<()> { match notification { + LspNotification::DidOpenTextDocument(params) => { + self.documents.handle_did_open(params.clone())?; + self.notifier.log_message( + MessageType::INFO, + &format!("Opened document: {}", params.text_document.uri), + )?; + Ok(()) + } + LspNotification::DidChangeTextDocument(params) => { + self.documents.handle_did_change(params.clone())?; + self.notifier.log_message( + MessageType::INFO, + &format!("Changed document: {}", params.text_document.uri), + )?; + Ok(()) + } + LspNotification::DidCloseTextDocument(params) => { + self.documents.handle_did_close(params.clone())?; + self.notifier.log_message( + MessageType::INFO, + &format!("Closed document: {}", params.text_document.uri), + )?; + Ok(()) + } LspNotification::Initialized(_) => { self.notifier .log_message(MessageType::INFO, "server initialized!")?;