create djls-cli and migrate serving LSP to it (#20)

This commit is contained in:
Josh Thomas 2024-12-11 10:23:35 -06:00 committed by GitHub
parent 2cbc24b5f0
commit 4c10afb602
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 46 additions and 4 deletions

View 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"

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

@ -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(())
}

View 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(())
}
}

View 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(()),
}
}
}

View 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)
}
}