mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-09 21:54:59 +00:00
swap from IPC architecture to PyO3 library (#45)
This commit is contained in:
parent
df30aafde5
commit
a73e912e0f
53 changed files with 136 additions and 2224 deletions
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "djls-django"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-ipc = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
|
@ -1,46 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App(String);
|
||||
|
||||
impl App {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for App {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Apps(Vec<App>);
|
||||
|
||||
impl Apps {
|
||||
pub fn from_strings(apps: Vec<String>) -> Self {
|
||||
Self(apps.into_iter().map(App).collect())
|
||||
}
|
||||
|
||||
pub fn apps(&self) -> &[App] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn has_app(&self, name: &str) -> bool {
|
||||
self.apps().iter().any(|app| app.0 == name)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &App> {
|
||||
self.apps().iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Apps {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for app in &self.0 {
|
||||
writeln!(f, " {}", app)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
use djls_ipc::v1::*;
|
||||
use djls_ipc::IpcCommand;
|
||||
use djls_ipc::{ProcessError, PythonProcess, TransportError};
|
||||
use djls_python::Python;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DjangoProject {
|
||||
py: Python,
|
||||
python: PythonProcess,
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
fn new(py: Python, python: PythonProcess, version: String) -> Self {
|
||||
Self {
|
||||
py,
|
||||
python,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
|
||||
let py = Python::setup(&mut python)?;
|
||||
|
||||
match commands::check::GeoDjangoPrereqsRequest::execute(&mut python)?.result {
|
||||
Some(messages::response::Result::CheckGeodjangoPrereqs(response)) => {
|
||||
if !response.passed {
|
||||
eprintln!("Warning: GeoDjango detected but GDAL is not available.");
|
||||
eprintln!(
|
||||
"Django initialization will be skipped. Some features may be limited."
|
||||
);
|
||||
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
|
||||
|
||||
return Ok(Self {
|
||||
py,
|
||||
python,
|
||||
version: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message))?,
|
||||
_ => Err(ProcessError::Response)?,
|
||||
}
|
||||
|
||||
let response = commands::django::GetProjectInfoRequest::execute(&mut python)?;
|
||||
|
||||
let version = match response.result {
|
||||
Some(messages::response::Result::DjangoGetProjectInfo(response)) => {
|
||||
response.project.unwrap().version
|
||||
}
|
||||
_ => {
|
||||
return Err(ProjectError::Process(ProcessError::Response));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
py,
|
||||
python,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn py(&self) -> &Python {
|
||||
&self.py
|
||||
}
|
||||
|
||||
fn version(&self) -> &String {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DjangoProject {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Django Project")?;
|
||||
writeln!(f, "Version: {}", self.version)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProjectError {
|
||||
#[error("Django is not installed or cannot be imported")]
|
||||
DjangoNotFound,
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Packaging(#[from] djls_python::PackagingError),
|
||||
#[error("Process error: {0}")]
|
||||
Process(#[from] ProcessError),
|
||||
#[error(transparent)]
|
||||
Python(#[from] djls_python::PythonError),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod apps;
|
||||
mod django;
|
||||
mod templates;
|
||||
|
||||
pub use django::DjangoProject;
|
|
@ -1,63 +0,0 @@
|
|||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TemplateTag {
|
||||
name: String,
|
||||
library: String,
|
||||
doc: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for TemplateTag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let library = if self.library.is_empty() {
|
||||
"builtins"
|
||||
} else {
|
||||
&self.library
|
||||
};
|
||||
|
||||
write!(f, "{} ({})", self.name, library)?;
|
||||
writeln!(f)?;
|
||||
|
||||
if let Some(doc) = &self.doc {
|
||||
for line in doc.trim_end().split("\n") {
|
||||
writeln!(f, "{}", line)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct TemplateTags(Vec<TemplateTag>);
|
||||
|
||||
impl TemplateTags {
|
||||
pub fn tags(&self) -> &Vec<TemplateTag> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = &TemplateTag> {
|
||||
self.tags().iter()
|
||||
}
|
||||
|
||||
pub fn filter_by_prefix<'a>(
|
||||
&'a self,
|
||||
prefix: &'a str,
|
||||
) -> impl Iterator<Item = &'a TemplateTag> {
|
||||
self.iter().filter(move |tag| tag.name.starts_with(prefix))
|
||||
}
|
||||
|
||||
pub fn get_by_name(&self, name: &str) -> Option<&TemplateTag> {
|
||||
self.iter().find(|tag| tag.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TemplateTags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for tag in &self.0 {
|
||||
writeln!(f, " {}", tag)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "djls-ipc"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
bytes = "1.9"
|
||||
prost = "0.13"
|
||||
tempfile = "3.14"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13"
|
|
@ -1,38 +0,0 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
struct Version(&'static str);
|
||||
|
||||
impl Version {
|
||||
fn collect_protos(&self, proto_root: &Path) -> Vec<PathBuf> {
|
||||
fs::read_dir(proto_root.join(self.0))
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("proto"))
|
||||
.map(|entry| entry.path())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
const VERSIONS: &[Version] = &[Version("v1")];
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_root = manifest_dir.parent().unwrap().parent().unwrap();
|
||||
let proto_dir = workspace_root.join("proto");
|
||||
|
||||
let mut protos = Vec::new();
|
||||
for version in VERSIONS {
|
||||
protos.extend(version.collect_protos(&proto_dir));
|
||||
}
|
||||
|
||||
prost_build::Config::new()
|
||||
.compile_protos(
|
||||
&protos
|
||||
.iter()
|
||||
.map(|p| p.to_str().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
&[proto_dir],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
use crate::proto::v1::{self, messages};
|
||||
use crate::{ProcessError, PythonProcess};
|
||||
|
||||
pub trait IpcCommand: Default {
|
||||
fn into_request(&self) -> messages::Request;
|
||||
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError>;
|
||||
|
||||
fn execute(process: &mut PythonProcess) -> Result<messages::Response, ProcessError> {
|
||||
let cmd = Self::default();
|
||||
let request = cmd.into_request();
|
||||
let response = process.send(request).map_err(ProcessError::Transport)?;
|
||||
Self::from_response(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcCommand for v1::commands::check::HealthRequest {
|
||||
fn into_request(&self) -> messages::Request {
|
||||
messages::Request {
|
||||
command: Some(messages::request::Command::CheckHealth(*self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
|
||||
match response.result {
|
||||
Some(messages::response::Result::CheckHealth(_)) => Ok(response),
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
|
||||
_ => Err(ProcessError::Response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcCommand for v1::commands::check::GeoDjangoPrereqsRequest {
|
||||
fn into_request(&self) -> messages::Request {
|
||||
messages::Request {
|
||||
command: Some(messages::request::Command::CheckGeodjangoPrereqs(*self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
|
||||
match response.result {
|
||||
Some(messages::response::Result::CheckGeodjangoPrereqs(_)) => Ok(response),
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
|
||||
_ => Err(ProcessError::Response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcCommand for v1::commands::python::GetEnvironmentRequest {
|
||||
fn into_request(&self) -> messages::Request {
|
||||
messages::Request {
|
||||
command: Some(messages::request::Command::PythonGetEnvironment(*self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
|
||||
match response.result {
|
||||
Some(messages::response::Result::PythonGetEnvironment(_)) => Ok(response),
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
|
||||
_ => Err(ProcessError::Response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcCommand for v1::commands::django::GetProjectInfoRequest {
|
||||
fn into_request(&self) -> messages::Request {
|
||||
messages::Request {
|
||||
command: Some(messages::request::Command::DjangoGetProjectInfo(*self)),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
|
||||
match response.result {
|
||||
Some(messages::response::Result::DjangoGetProjectInfo(_)) => Ok(response),
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
|
||||
_ => Err(ProcessError::Response),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
mod commands;
|
||||
mod process;
|
||||
mod proto;
|
||||
mod transport;
|
||||
|
||||
pub use commands::IpcCommand;
|
||||
pub use process::ProcessError;
|
||||
pub use process::PythonProcess;
|
||||
pub use proto::v1;
|
||||
pub use transport::Transport;
|
||||
pub use transport::TransportError;
|
|
@ -1,138 +0,0 @@
|
|||
use crate::proto::v1::*;
|
||||
use crate::transport::{Transport, TransportError};
|
||||
use std::ffi::OsStr;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PythonProcess {
|
||||
transport: Arc<Mutex<Transport>>,
|
||||
_child: Child,
|
||||
healthy: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PythonProcess {
|
||||
pub fn new<I, S>(
|
||||
module: &str,
|
||||
args: Option<I>,
|
||||
health_check_interval: Option<Duration>,
|
||||
) -> Result<Self, ProcessError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let mut command = Command::new("python");
|
||||
command.arg("-m").arg(module);
|
||||
|
||||
if let Some(args) = args {
|
||||
command.args(args);
|
||||
}
|
||||
|
||||
command.stdin(Stdio::piped()).stdout(Stdio::piped());
|
||||
|
||||
let mut child = command.spawn().map_err(TransportError::Io)?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
let transport = Transport::new(stdin, stdout)?;
|
||||
|
||||
let process = Self {
|
||||
transport: Arc::new(Mutex::new(transport)),
|
||||
_child: child,
|
||||
healthy: Arc::new(AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
if let Some(interval) = health_check_interval {
|
||||
let transport = process.transport.clone();
|
||||
let healthy = process.healthy.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let _ = PythonProcess::check_health(transport.clone(), healthy.clone()).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
pub fn is_healthy(&self) -> bool {
|
||||
self.healthy.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn send(
|
||||
&mut self,
|
||||
request: messages::Request,
|
||||
) -> Result<messages::Response, TransportError> {
|
||||
let mut transport = self.transport.lock().unwrap();
|
||||
transport.send(request)
|
||||
}
|
||||
|
||||
async fn check_health(
|
||||
transport: Arc<Mutex<Transport>>,
|
||||
healthy: Arc<AtomicBool>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let request = messages::Request {
|
||||
command: Some(messages::request::Command::CheckHealth(
|
||||
commands::check::HealthRequest {},
|
||||
)),
|
||||
};
|
||||
|
||||
let response = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut transport = transport.lock().unwrap();
|
||||
transport.send(request)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ProcessError::Timeout(5))?
|
||||
.map_err(TransportError::Task)?
|
||||
.map_err(ProcessError::Transport)?;
|
||||
|
||||
let result = match response.result {
|
||||
Some(messages::response::Result::CheckHealth(health)) => {
|
||||
if !health.passed {
|
||||
let error_msg = health.error.unwrap_or_else(|| "Unknown error".to_string());
|
||||
Err(ProcessError::Health(error_msg))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
|
||||
_ => Err(ProcessError::Response),
|
||||
};
|
||||
|
||||
healthy.store(result.is_ok(), Ordering::SeqCst);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PythonProcess {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(()) = self._child.kill() {
|
||||
let _ = self._child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProcessError {
|
||||
#[error("Health check failed: {0}")]
|
||||
Health(String),
|
||||
#[error("Operation timed out after {0} seconds")]
|
||||
Timeout(u64),
|
||||
#[error("Unexpected response type")]
|
||||
Response,
|
||||
#[error("Failed to acquire lock: {0}")]
|
||||
Lock(String),
|
||||
#[error("Process not ready: {0}")]
|
||||
Ready(String),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
pub mod v1 {
|
||||
pub mod commands {
|
||||
include!(concat!(env!("OUT_DIR"), "/djls.v1.commands.rs"));
|
||||
}
|
||||
|
||||
pub mod django {
|
||||
include!(concat!(env!("OUT_DIR"), "/djls.v1.django.rs"));
|
||||
}
|
||||
|
||||
pub mod messages {
|
||||
include!(concat!(env!("OUT_DIR"), "/djls.v1.messages.rs"));
|
||||
}
|
||||
|
||||
pub mod python {
|
||||
include!(concat!(env!("OUT_DIR"), "/djls.v1.python.rs"));
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
use crate::process::ProcessError;
|
||||
use crate::proto::v1::*;
|
||||
use prost::Message;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
|
||||
use std::process::{ChildStdin, ChildStdout};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Transport {
|
||||
reader: Arc<Mutex<BufReader<ChildStdout>>>,
|
||||
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn new(mut stdin: ChildStdin, mut stdout: ChildStdout) -> Result<Self, ProcessError> {
|
||||
stdin.flush().map_err(TransportError::Io)?;
|
||||
|
||||
let mut ready_line = String::new();
|
||||
BufReader::new(&mut stdout)
|
||||
.read_line(&mut ready_line)
|
||||
.map_err(TransportError::Io)?;
|
||||
|
||||
if ready_line.trim() != "ready" {
|
||||
return Err(ProcessError::Ready("Python process not ready".to_string()));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
|
||||
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(
|
||||
&mut self,
|
||||
message: messages::Request,
|
||||
) -> Result<messages::Response, TransportError> {
|
||||
let buf = message.encode_to_vec();
|
||||
|
||||
let mut writer = self.writer.lock().map_err(|_| {
|
||||
TransportError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to acquire writer lock",
|
||||
))
|
||||
})?;
|
||||
writer
|
||||
.write_all(&(buf.len() as u32).to_be_bytes())
|
||||
.map_err(TransportError::Io)?;
|
||||
writer.write_all(&buf).map_err(TransportError::Io)?;
|
||||
writer.flush().map_err(TransportError::Io)?;
|
||||
|
||||
let mut reader = self.reader.lock().map_err(|_| {
|
||||
TransportError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to acquire reader lock",
|
||||
))
|
||||
})?;
|
||||
let mut length_bytes = [0u8; 4];
|
||||
reader
|
||||
.read_exact(&mut length_bytes)
|
||||
.map_err(TransportError::Io)?;
|
||||
let length = u32::from_be_bytes(length_bytes);
|
||||
|
||||
let mut message_bytes = vec![0u8; length as usize];
|
||||
reader
|
||||
.read_exact(&mut message_bytes)
|
||||
.map_err(TransportError::Io)?;
|
||||
|
||||
messages::Response::decode(message_bytes.as_slice())
|
||||
.map_err(|e| TransportError::Decode(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum TransportError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Task error: {0}")]
|
||||
Task(#[from] tokio::task::JoinError),
|
||||
#[error("Failed to decode message: {0}")]
|
||||
Decode(String),
|
||||
}
|
47
crates/djls-ipc/tests/fixtures/echo_server.py
vendored
47
crates/djls-ipc/tests/fixtures/echo_server.py
vendored
|
@ -1,47 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
async def handle_client(reader, writer):
|
||||
while True:
|
||||
try:
|
||||
data = await reader.readline()
|
||||
if not data:
|
||||
break
|
||||
|
||||
# Parse the incoming message
|
||||
message = json.loads(data)
|
||||
# Echo back with same ID but just echo the content
|
||||
response = {"id": message["id"], "content": message["content"]}
|
||||
writer.write(json.dumps(response).encode() + b"\n")
|
||||
await writer.drain()
|
||||
except Exception:
|
||||
break
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
|
||||
async def main(ipc_path):
|
||||
try:
|
||||
Path(ipc_path).unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
server = await asyncio.start_unix_server(
|
||||
handle_client,
|
||||
path=ipc_path,
|
||||
)
|
||||
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--ipc-path", required=True)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(args.ipc_path))
|
|
@ -1,13 +0,0 @@
|
|||
[package]
|
||||
name = "djls-python"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-ipc = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
which = "7.0"
|
|
@ -1,6 +0,0 @@
|
|||
mod packaging;
|
||||
mod python;
|
||||
|
||||
pub use crate::packaging::PackagingError;
|
||||
pub use crate::python::Python;
|
||||
pub use crate::python::PythonError;
|
|
@ -1,87 +0,0 @@
|
|||
use djls_ipc::v1::*;
|
||||
use djls_ipc::{ProcessError, TransportError};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Package {
|
||||
dist_name: String,
|
||||
dist_version: String,
|
||||
dist_location: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<python::Package> for Package {
|
||||
fn from(p: python::Package) -> Self {
|
||||
Package {
|
||||
dist_name: p.dist_name,
|
||||
dist_version: p.dist_version,
|
||||
dist_location: p.dist_location.map(PathBuf::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Package {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {}", self.dist_name, self.dist_version)?;
|
||||
if let Some(location) = &self.dist_location {
|
||||
write!(f, " ({})", location.display())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Packages(HashMap<String, Package>);
|
||||
|
||||
impl Packages {
|
||||
pub fn packages(&self) -> Vec<&Package> {
|
||||
self.0.values().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HashMap<String, python::Package>> for Packages {
|
||||
fn from(packages: HashMap<String, python::Package>) -> Self {
|
||||
Packages(packages.into_iter().map(|(k, v)| (k, v.into())).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(String, Package)> for Packages {
|
||||
fn from_iter<T: IntoIterator<Item = (String, Package)>>(iter: T) -> Self {
|
||||
Self(HashMap::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Packages {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut packages: Vec<_> = self.packages();
|
||||
packages.sort_by(|a, b| a.dist_name.cmp(&b.dist_name));
|
||||
|
||||
if packages.is_empty() {
|
||||
writeln!(f, " (no packages installed)")?;
|
||||
} else {
|
||||
for package in packages {
|
||||
writeln!(f, "{}", package)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PackagingError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
#[error("Process error: {0}")]
|
||||
Process(#[from] ProcessError),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
use crate::packaging::{Packages, PackagingError};
|
||||
use djls_ipc::v1::*;
|
||||
use djls_ipc::IpcCommand;
|
||||
use djls_ipc::{ProcessError, PythonProcess, TransportError};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct VersionInfo {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
micro: u8,
|
||||
releaselevel: ReleaseLevel,
|
||||
serial: Option<String>,
|
||||
}
|
||||
|
||||
impl From<python::VersionInfo> for VersionInfo {
|
||||
fn from(v: python::VersionInfo) -> Self {
|
||||
Self {
|
||||
major: v.major as u8,
|
||||
minor: v.minor as u8,
|
||||
micro: v.micro as u8,
|
||||
releaselevel: v.releaselevel().into(),
|
||||
serial: Some(v.serial.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub enum ReleaseLevel {
|
||||
Alpha,
|
||||
Beta,
|
||||
Candidate,
|
||||
Final,
|
||||
}
|
||||
|
||||
impl From<python::ReleaseLevel> for ReleaseLevel {
|
||||
fn from(level: python::ReleaseLevel) -> Self {
|
||||
match level {
|
||||
python::ReleaseLevel::Alpha => ReleaseLevel::Alpha,
|
||||
python::ReleaseLevel::Beta => ReleaseLevel::Beta,
|
||||
python::ReleaseLevel::Candidate => ReleaseLevel::Candidate,
|
||||
python::ReleaseLevel::Final => ReleaseLevel::Final,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SysconfigPaths {
|
||||
data: PathBuf,
|
||||
include: PathBuf,
|
||||
platinclude: PathBuf,
|
||||
platlib: PathBuf,
|
||||
platstdlib: PathBuf,
|
||||
purelib: PathBuf,
|
||||
scripts: PathBuf,
|
||||
stdlib: PathBuf,
|
||||
}
|
||||
|
||||
impl fmt::Display for SysconfigPaths {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "data: {}", self.data.display())?;
|
||||
writeln!(f, "include: {}", self.include.display())?;
|
||||
writeln!(f, "platinclude: {}", self.platinclude.display())?;
|
||||
writeln!(f, "platlib: {}", self.platlib.display())?;
|
||||
writeln!(f, "platstdlib: {}", self.platstdlib.display())?;
|
||||
writeln!(f, "purelib: {}", self.purelib.display())?;
|
||||
writeln!(f, "scripts: {}", self.scripts.display())?;
|
||||
write!(f, "stdlib: {}", self.stdlib.display())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Python {
|
||||
version_info: VersionInfo,
|
||||
sysconfig_paths: SysconfigPaths,
|
||||
sys_prefix: PathBuf,
|
||||
sys_base_prefix: PathBuf,
|
||||
sys_executable: PathBuf,
|
||||
sys_path: Vec<PathBuf>,
|
||||
packages: Packages,
|
||||
}
|
||||
|
||||
impl Python {
|
||||
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
|
||||
let response = commands::python::GetEnvironmentRequest::execute(python)?;
|
||||
match response.result {
|
||||
Some(messages::response::Result::PythonGetEnvironment(response)) => response
|
||||
.python
|
||||
.ok_or_else(|| PythonError::Process(ProcessError::Response))
|
||||
.map(Into::into),
|
||||
_ => Err(PythonError::Process(ProcessError::Response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<python::Python> for Python {
|
||||
fn from(p: python::Python) -> Self {
|
||||
let sys = p.sys.unwrap();
|
||||
let sysconfig = p.sysconfig.unwrap();
|
||||
let site = p.site.unwrap();
|
||||
|
||||
Self {
|
||||
version_info: sys.version_info.unwrap_or_default().into(),
|
||||
sysconfig_paths: SysconfigPaths {
|
||||
data: PathBuf::from(sysconfig.data),
|
||||
include: PathBuf::from(sysconfig.include),
|
||||
platinclude: PathBuf::from(sysconfig.platinclude),
|
||||
platlib: PathBuf::from(sysconfig.platlib),
|
||||
platstdlib: PathBuf::from(sysconfig.platstdlib),
|
||||
purelib: PathBuf::from(sysconfig.purelib),
|
||||
scripts: PathBuf::from(sysconfig.scripts),
|
||||
stdlib: PathBuf::from(sysconfig.stdlib),
|
||||
},
|
||||
sys_prefix: PathBuf::from(sys.prefix),
|
||||
sys_base_prefix: PathBuf::from(sys.base_prefix),
|
||||
sys_executable: PathBuf::from(sys.executable),
|
||||
sys_path: sys.path.into_iter().map(PathBuf::from).collect(),
|
||||
packages: site.packages.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Python {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Version: {}", self.version_info)?;
|
||||
writeln!(f, "Executable: {}", self.sys_executable.display())?;
|
||||
writeln!(f, "Prefix: {}", self.sys_prefix.display())?;
|
||||
writeln!(f, "Base Prefix: {}", self.sys_base_prefix.display())?;
|
||||
writeln!(f, "Paths:")?;
|
||||
for path in &self.sys_path {
|
||||
writeln!(f, "{}", path.display())?;
|
||||
}
|
||||
writeln!(f, "Sysconfig Paths:")?;
|
||||
write!(f, "{}", self.sysconfig_paths)?;
|
||||
writeln!(f, "\nInstalled Packages:")?;
|
||||
write!(f, "{}", self.packages)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PythonError {
|
||||
#[error("Python execution failed: {0}")]
|
||||
Execution(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Packaging error: {0}")]
|
||||
Packaging(#[from] PackagingError),
|
||||
#[error("Integer parsing error: {0}")]
|
||||
Parse(#[from] std::num::ParseIntError),
|
||||
#[error("Process error: {0}")]
|
||||
Process(#[from] ProcessError),
|
||||
#[error("Failed to locate Python executable: {0}")]
|
||||
PythonNotFound(#[from] which::Error),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
|
@ -4,9 +4,6 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-django = { workspace = true }
|
||||
djls-ipc = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
djls-template-ast = { workspace = true }
|
||||
djls-worker = { workspace = true }
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@ mod tasks;
|
|||
use crate::notifier::TowerLspNotifier;
|
||||
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
||||
use anyhow::Result;
|
||||
use djls_django::DjangoProject;
|
||||
use djls_ipc::PythonProcess;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_lsp::jsonrpc::Result as LspResult;
|
||||
|
@ -81,15 +79,13 @@ impl LanguageServer for TowerLspBackend {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn serve(python: PythonProcess) -> Result<()> {
|
||||
let django = DjangoProject::setup(python)?;
|
||||
|
||||
pub async fn serve() -> Result<()> {
|
||||
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);
|
||||
let server = DjangoLanguageServer::new(notifier);
|
||||
TowerLspBackend {
|
||||
server: Arc::new(RwLock::new(server)),
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ 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;
|
||||
|
@ -24,18 +23,16 @@ pub enum LspNotification {
|
|||
}
|
||||
|
||||
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 {
|
||||
pub fn new(notifier: Box<dyn Notifier>) -> Self {
|
||||
let notifier = Arc::new(notifier);
|
||||
|
||||
Self {
|
||||
django,
|
||||
notifier,
|
||||
documents: Store::new(),
|
||||
worker: Worker::new(),
|
||||
|
@ -125,10 +122,6 @@ impl DjangoLanguageServer {
|
|||
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(()),
|
||||
|
|
|
@ -3,12 +3,16 @@ name = "djls"
|
|||
version = "5.1.0-alpha.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "djls"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
djls-django = { workspace = true }
|
||||
djls-ipc = { workspace = true }
|
||||
djls-server = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
pyo3 = { workspace = true, features = ["extension-module"] }
|
||||
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
|
|
113
crates/djls/src/lib.rs
Normal file
113
crates/djls/src/lib.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
mod commands;
|
||||
|
||||
use crate::commands::Serve;
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use pyo3::prelude::*;
|
||||
use std::env;
|
||||
use std::process::ExitCode;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "djls")]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
|
||||
#[command(flatten)]
|
||||
args: Args,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Start the LSP server
|
||||
Serve(Serve),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[command(flatten)]
|
||||
global: GlobalArgs,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct GlobalArgs {
|
||||
/// Do not print any output.
|
||||
#[arg(global = true, long, short, conflicts_with = "verbose")]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Use verbose output.
|
||||
#[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")]
|
||||
pub verbose: u8,
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn cli_entrypoint(_py: Python) -> PyResult<()> {
|
||||
// Skip python interpreter and script path, add command name
|
||||
let args: Vec<String> = std::iter::once("djls".to_string())
|
||||
.chain(env::args().skip(2))
|
||||
.collect();
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.block_on(&runtime, async move {
|
||||
tokio::select! {
|
||||
// The main CLI program
|
||||
result = cli_main(args) => {
|
||||
match result {
|
||||
Ok(code) => {
|
||||
if code != ExitCode::SUCCESS {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok::<(), PyErr>(())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
if let Some(source) = e.source() {
|
||||
eprintln!("Caused by: {}", source);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ctrl+C handling
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!("\nReceived Ctrl+C, shutting down...");
|
||||
// Cleanup code here if needed
|
||||
std::process::exit(130); // Standard Ctrl+C exit code
|
||||
}
|
||||
// SIGTERM handling (Unix only)
|
||||
_ = async {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut term = signal(SignalKind::terminate()).unwrap();
|
||||
term.recv().await;
|
||||
}
|
||||
} => {
|
||||
println!("\nReceived termination signal, shutting down...");
|
||||
std::process::exit(143); // Standard SIGTERM exit code
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cli_main(args: Vec<String>) -> Result<ExitCode> {
|
||||
let cli = Cli::try_parse_from(args).unwrap_or_else(|e| {
|
||||
e.exit();
|
||||
});
|
||||
|
||||
match cli.command {
|
||||
Command::Serve(_serve) => djls_server::serve().await?,
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn djls(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(cli_entrypoint, m)?)?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
mod commands;
|
||||
|
||||
use crate::commands::Serve;
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use djls_ipc::PythonProcess;
|
||||
use std::ffi::OsStr;
|
||||
use std::process::ExitCode;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "djls")]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
|
||||
#[command(flatten)]
|
||||
args: Args,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Start the LSP server
|
||||
Serve(Serve),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[command(flatten)]
|
||||
global: GlobalArgs,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct GlobalArgs {
|
||||
/// Do not print any output.
|
||||
#[arg(global = true, long, short, conflicts_with = "verbose")]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Use verbose output.
|
||||
#[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")]
|
||||
pub verbose: u8,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<ExitCode> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Serve(_serve) => {
|
||||
let python = PythonProcess::new::<Vec<&OsStr>, &OsStr>("djls_agent", None, None)?;
|
||||
djls_server::serve(python).await?
|
||||
}
|
||||
}
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue