add document store and support for didopen, didchange, and didclose (#11)

This commit is contained in:
Josh Thomas 2024-12-09 23:34:55 -06:00 committed by GitHub
parent 5971c23ace
commit 1799355e27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 341 additions and 9 deletions

View file

@ -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<String, TextDocument>,
versions: HashMap<String, i32>,
}
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<Item = &TextDocument> {
self.documents.values()
}
pub fn get_documents_by_language(
&self,
language_id: LanguageId,
) -> impl Iterator<Item = &TextDocument> {
self.documents
.values()
.filter(move |doc| doc.language_id == language_id)
}
pub fn get_version(&self, uri: &str) -> Option<i32> {
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<u32>,
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<u32> {
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<String> for LanguageId {
fn from(language_id: String) -> Self {
Self::from(language_id.as_str())
}
}

View file

@ -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<RwLock<DjangoLanguageServer>>,
}
#[tower_lsp::async_trait]
impl LanguageServer for TowerLspBackend {
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
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();

View file

@ -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<dyn Notifier>,
documents: Store,
}
impl DjangoLanguageServer {
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
Self { django, notifier }
Self {
django,
notifier,
documents: Store::new(),
}
}
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
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!")?;