mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-08-04 01:58:18 +00:00
create djls-cli and migrate serving LSP to it (#20)
This commit is contained in:
parent
2cbc24b5f0
commit
4c10afb602
9 changed files with 46 additions and 4 deletions
18
crates/djls-server/Cargo.toml
Normal file
18
crates/djls-server/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "djls-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-ast = { workspace = true }
|
||||
djls-django = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
djls-worker = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
lsp-types = "0.97.0"
|
250
crates/djls-server/src/documents.rs
Normal file
250
crates/djls-server/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())
|
||||
}
|
||||
}
|
101
crates/djls-server/src/lib.rs
Normal file
101
crates/djls-server/src/lib.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
mod documents;
|
||||
mod notifier;
|
||||
mod server;
|
||||
mod tasks;
|
||||
|
||||
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: 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 let Err(e) = self
|
||||
.server
|
||||
.write()
|
||||
.await
|
||||
.handle_notification(LspNotification::Initialized(params))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve() -> Result<()> {
|
||||
let django = DjangoProject::setup()?;
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
||||
let (service, socket) = LspService::build(|client| {
|
||||
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
|
||||
let server = DjangoLanguageServer::new(django, notifier);
|
||||
TowerLspBackend {
|
||||
server: Arc::new(RwLock::new(server)),
|
||||
}
|
||||
})
|
||||
.finish();
|
||||
|
||||
Server::new(stdin, stdout, socket).serve(service).await;
|
||||
|
||||
Ok(())
|
||||
}
|
80
crates/djls-server/src/notifier.rs
Normal file
80
crates/djls-server/src/notifier.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use anyhow::Result;
|
||||
use tower_lsp::async_trait;
|
||||
use tower_lsp::lsp_types::Diagnostic;
|
||||
use tower_lsp::lsp_types::MessageActionItem;
|
||||
use tower_lsp::lsp_types::MessageType;
|
||||
use tower_lsp::lsp_types::Url;
|
||||
use tower_lsp::Client;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Notifier: Send + Sync {
|
||||
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()>;
|
||||
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()>;
|
||||
async fn show_message_request(
|
||||
&self,
|
||||
typ: MessageType,
|
||||
msg: &str,
|
||||
actions: Option<Vec<MessageActionItem>>,
|
||||
) -> Result<Option<MessageActionItem>>;
|
||||
fn publish_diagnostics(
|
||||
&self,
|
||||
uri: Url,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
version: Option<i32>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct TowerLspNotifier {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl TowerLspNotifier {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Notifier for TowerLspNotifier {
|
||||
fn log_message(&self, typ: MessageType, msg: &str) -> Result<()> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
tokio::spawn(async move {
|
||||
client.log_message(typ, msg).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_message(&self, typ: MessageType, msg: &str) -> Result<()> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
tokio::spawn(async move {
|
||||
client.show_message(typ, msg).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_message_request(
|
||||
&self,
|
||||
typ: MessageType,
|
||||
msg: &str,
|
||||
actions: Option<Vec<MessageActionItem>>,
|
||||
) -> Result<Option<MessageActionItem>> {
|
||||
let client = self.client.clone();
|
||||
let msg = msg.to_string();
|
||||
Ok(client.show_message_request(typ, msg, actions).await?)
|
||||
}
|
||||
|
||||
fn publish_diagnostics(
|
||||
&self,
|
||||
uri: Url,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
version: Option<i32>,
|
||||
) -> Result<()> {
|
||||
let client = self.client.clone();
|
||||
tokio::spawn(async move {
|
||||
client.publish_diagnostics(uri, diagnostics, version).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
137
crates/djls-server/src/server.rs
Normal file
137
crates/djls-server/src/server.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use crate::documents::Store;
|
||||
use crate::notifier::Notifier;
|
||||
use crate::tasks::DebugTask;
|
||||
use anyhow::Result;
|
||||
use djls_django::DjangoProject;
|
||||
use djls_worker::Worker;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tower_lsp::lsp_types::*;
|
||||
|
||||
const SERVER_NAME: &str = "Django Language Server";
|
||||
const SERVER_VERSION: &str = "0.1.0";
|
||||
|
||||
pub enum LspRequest {
|
||||
Initialize(InitializeParams),
|
||||
}
|
||||
|
||||
pub enum LspNotification {
|
||||
DidOpenTextDocument(DidOpenTextDocumentParams),
|
||||
DidChangeTextDocument(DidChangeTextDocumentParams),
|
||||
DidCloseTextDocument(DidCloseTextDocumentParams),
|
||||
Initialized(InitializedParams),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct DjangoLanguageServer {
|
||||
django: DjangoProject,
|
||||
notifier: Arc<Box<dyn Notifier>>,
|
||||
documents: Store,
|
||||
worker: Worker,
|
||||
}
|
||||
|
||||
impl DjangoLanguageServer {
|
||||
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
|
||||
let notifier = Arc::new(notifier);
|
||||
|
||||
Self {
|
||||
django,
|
||||
notifier,
|
||||
documents: Store::new(),
|
||||
worker: Worker::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_request(&self, request: LspRequest) -> Result<InitializeResult> {
|
||||
match request {
|
||||
LspRequest::Initialize(_params) => Ok(InitializeResult {
|
||||
capabilities: ServerCapabilities {
|
||||
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()
|
||||
},
|
||||
offset_encoding: None,
|
||||
server_info: Some(ServerInfo {
|
||||
name: SERVER_NAME.to_string(),
|
||||
version: Some(SERVER_VERSION.to_string()),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
)?;
|
||||
|
||||
// Execute - still sync
|
||||
self.worker.execute(DebugTask::new(
|
||||
"Quick task".to_string(),
|
||||
Duration::from_millis(100),
|
||||
self.notifier.clone(),
|
||||
))?;
|
||||
|
||||
// Submit - spawn async task
|
||||
let worker = self.worker.clone();
|
||||
let task = DebugTask::new(
|
||||
"Important task".to_string(),
|
||||
Duration::from_secs(1),
|
||||
self.notifier.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let _ = worker.submit(task).await;
|
||||
});
|
||||
|
||||
// Wait for result - spawn async task
|
||||
let worker = self.worker.clone();
|
||||
let task = DebugTask::new(
|
||||
"Task with result".to_string(),
|
||||
Duration::from_secs(2),
|
||||
self.notifier.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let _ = worker.wait_for(task).await;
|
||||
});
|
||||
|
||||
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!")?;
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, &format!("\n{}", self.django.py()))?;
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, &format!("\n{}", self.django))?;
|
||||
Ok(())
|
||||
}
|
||||
LspNotification::Shutdown => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
38
crates/djls-server/src/tasks.rs
Normal file
38
crates/djls-server/src/tasks.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::notifier::Notifier;
|
||||
use anyhow::Result;
|
||||
use djls_worker::Task;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tower_lsp::lsp_types::MessageType;
|
||||
|
||||
pub struct DebugTask {
|
||||
pub message: String,
|
||||
pub delay: Duration,
|
||||
pub notifier: Arc<Box<dyn Notifier>>,
|
||||
}
|
||||
|
||||
impl DebugTask {
|
||||
pub fn new(message: String, delay: Duration, notifier: Arc<Box<dyn Notifier>>) -> Self {
|
||||
Self {
|
||||
message,
|
||||
delay,
|
||||
notifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for DebugTask {
|
||||
type Output = String;
|
||||
|
||||
fn run(&self) -> Result<Self::Output> {
|
||||
std::thread::sleep(self.delay);
|
||||
let result = format!("Debug task completed: {}", self.message);
|
||||
|
||||
// Log the result
|
||||
self.notifier
|
||||
.log_message(MessageType::INFO, &result)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue