mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-11 12:56:46 +00:00
add document store and support for didopen, didchange, and didclose (#11)
This commit is contained in:
parent
5971c23ace
commit
1799355e27
3 changed files with 341 additions and 9 deletions
250
crates/djls/src/documents.rs
Normal file
250
crates/djls/src/documents.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod documents;
|
||||||
mod notifier;
|
mod notifier;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
|
@ -5,37 +6,77 @@ use crate::notifier::TowerLspNotifier;
|
||||||
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use djls_django::DjangoProject;
|
use djls_django::DjangoProject;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tower_lsp::jsonrpc::Result as LspResult;
|
use tower_lsp::jsonrpc::Result as LspResult;
|
||||||
use tower_lsp::lsp_types::*;
|
use tower_lsp::lsp_types::*;
|
||||||
use tower_lsp::{LanguageServer, LspService, Server};
|
use tower_lsp::{LanguageServer, LspService, Server};
|
||||||
|
|
||||||
struct TowerLspBackend {
|
struct TowerLspBackend {
|
||||||
server: DjangoLanguageServer,
|
server: Arc<RwLock<DjangoLanguageServer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tower_lsp::async_trait]
|
#[tower_lsp::async_trait]
|
||||||
impl LanguageServer for TowerLspBackend {
|
impl LanguageServer for TowerLspBackend {
|
||||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||||
self.server
|
self.server
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.handle_request(LspRequest::Initialize(params))
|
.handle_request(LspRequest::Initialize(params))
|
||||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn initialized(&self, params: InitializedParams) {
|
async fn initialized(&self, params: InitializedParams) {
|
||||||
if self
|
if let Err(e) = self
|
||||||
.server
|
.server
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
.handle_notification(LspNotification::Initialized(params))
|
.handle_notification(LspNotification::Initialized(params))
|
||||||
.is_err()
|
|
||||||
{
|
{
|
||||||
// Handle error
|
eprintln!("Error handling initialized: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown(&self) -> LspResult<()> {
|
async fn shutdown(&self) -> LspResult<()> {
|
||||||
self.server
|
self.server
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
.handle_notification(LspNotification::Shutdown)
|
.handle_notification(LspNotification::Shutdown)
|
||||||
.map_err(|_| tower_lsp::jsonrpc::Error::internal_error())
|
.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]
|
#[tokio::main]
|
||||||
|
@ -48,7 +89,9 @@ async fn main() -> Result<()> {
|
||||||
let (service, socket) = LspService::build(|client| {
|
let (service, socket) = LspService::build(|client| {
|
||||||
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
|
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
|
||||||
let server = DjangoLanguageServer::new(django, notifier);
|
let server = DjangoLanguageServer::new(django, notifier);
|
||||||
TowerLspBackend { server }
|
TowerLspBackend {
|
||||||
|
server: Arc::new(RwLock::new(server)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::documents::Store;
|
||||||
use crate::notifier::Notifier;
|
use crate::notifier::Notifier;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use djls_django::DjangoProject;
|
use djls_django::DjangoProject;
|
||||||
|
@ -11,6 +12,9 @@ pub enum LspRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LspNotification {
|
pub enum LspNotification {
|
||||||
|
DidOpenTextDocument(DidOpenTextDocumentParams),
|
||||||
|
DidChangeTextDocument(DidChangeTextDocumentParams),
|
||||||
|
DidCloseTextDocument(DidCloseTextDocumentParams),
|
||||||
Initialized(InitializedParams),
|
Initialized(InitializedParams),
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
@ -18,19 +22,30 @@ pub enum LspNotification {
|
||||||
pub struct DjangoLanguageServer {
|
pub struct DjangoLanguageServer {
|
||||||
django: DjangoProject,
|
django: DjangoProject,
|
||||||
notifier: Box<dyn Notifier>,
|
notifier: Box<dyn Notifier>,
|
||||||
|
documents: Store,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DjangoLanguageServer {
|
impl DjangoLanguageServer {
|
||||||
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
|
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> {
|
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
|
||||||
match request {
|
match request {
|
||||||
LspRequest::Initialize(_params) => Ok(InitializeResult {
|
LspRequest::Initialize(_params) => Ok(InitializeResult {
|
||||||
capabilities: ServerCapabilities {
|
capabilities: ServerCapabilities {
|
||||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||||
TextDocumentSyncKind::INCREMENTAL,
|
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()
|
..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 {
|
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(_) => {
|
LspNotification::Initialized(_) => {
|
||||||
self.notifier
|
self.notifier
|
||||||
.log_message(MessageType::INFO, "server initialized!")?;
|
.log_message(MessageType::INFO, "server initialized!")?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue