mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-19 18:35:03 +00:00
switch from runner to ipc and long-running sidecar process (#21)
This commit is contained in:
parent
4c10afb602
commit
235bb4419d
23 changed files with 556 additions and 281 deletions
|
@ -5,6 +5,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
djls-django = { workspace = true }
|
||||
djls-ipc = { workspace = true }
|
||||
djls-server = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use djls_ipc::{PythonProcess, Transport};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
|
@ -6,10 +8,35 @@ struct Cli {
|
|||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct CommonOpts {
|
||||
/// Disable periodic health checks
|
||||
#[arg(long)]
|
||||
no_health_check: bool,
|
||||
|
||||
/// Health check interval in seconds
|
||||
#[arg(long, default_value = "30")]
|
||||
health_interval: u64,
|
||||
}
|
||||
|
||||
impl CommonOpts {
|
||||
fn health_check_interval(&self) -> Option<Duration> {
|
||||
if self.no_health_check {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(self.health_interval))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Start the LSP server
|
||||
Serve,
|
||||
Serve(CommonOpts),
|
||||
/// Get Python environment information
|
||||
Info(CommonOpts),
|
||||
/// Print the version
|
||||
Version(CommonOpts),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -17,7 +44,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Serve => djls_server::serve().await?,
|
||||
Commands::Serve(opts) => {
|
||||
let python =
|
||||
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
|
||||
djls_server::serve(python).await?
|
||||
}
|
||||
Commands::Info(opts) => {
|
||||
let mut python =
|
||||
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
|
||||
match python.send("python_setup", None) {
|
||||
Ok(info) => println!("{}", info),
|
||||
Err(e) => eprintln!("Failed to get info: {}", e),
|
||||
}
|
||||
}
|
||||
Commands::Version(opts) => {
|
||||
let mut python =
|
||||
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
|
||||
match python.send("version", None) {
|
||||
Ok(version) => println!("Python module version: {}", version),
|
||||
Err(e) => eprintln!("Failed to get version: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -4,6 +4,7 @@ version = "0.0.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-ipc = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use djls_python::{Python, RunnerError, ScriptRunner};
|
||||
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::scripts;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App(String);
|
||||
|
||||
|
@ -27,8 +25,16 @@ struct InstalledAppsCheck {
|
|||
has_app: bool,
|
||||
}
|
||||
|
||||
impl ScriptRunner for InstalledAppsCheck {
|
||||
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK;
|
||||
impl TryFrom<JsonResponse> for InstalledAppsCheck {
|
||||
type Error = TransportError;
|
||||
|
||||
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
|
||||
response
|
||||
.data()
|
||||
.clone()
|
||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
|
||||
}
|
||||
}
|
||||
|
||||
impl Apps {
|
||||
|
@ -48,8 +54,10 @@ impl Apps {
|
|||
self.apps().iter()
|
||||
}
|
||||
|
||||
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> {
|
||||
let result = InstalledAppsCheck::run_with_py_args(py, app)?;
|
||||
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
|
||||
let response = python.send("installed_apps_check", Some(vec![app.to_string()]))?;
|
||||
let response = parse_json_response(response)?;
|
||||
let result = InstalledAppsCheck::try_from(response)?;
|
||||
Ok(result.has_app)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use crate::apps::Apps;
|
||||
use crate::gis::{check_gis_setup, GISError};
|
||||
use crate::scripts;
|
||||
use crate::templates::TemplateTags;
|
||||
use djls_python::{ImportCheck, Python, RunnerError, ScriptRunner};
|
||||
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||
use djls_python::{ImportCheck, Python};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DjangoProject {
|
||||
py: Python,
|
||||
python: PythonProcess,
|
||||
settings_module: String,
|
||||
installed_apps: Apps,
|
||||
templatetags: TemplateTags,
|
||||
|
@ -20,54 +21,67 @@ struct DjangoSetup {
|
|||
templatetags: TemplateTags,
|
||||
}
|
||||
|
||||
impl ScriptRunner for DjangoSetup {
|
||||
const SCRIPT: &'static str = scripts::DJANGO_SETUP;
|
||||
impl DjangoSetup {
|
||||
pub fn setup(python: &mut PythonProcess) -> Result<JsonResponse, ProjectError> {
|
||||
let response = python.send("django_setup", None)?;
|
||||
let response = parse_json_response(response)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
fn new(
|
||||
py: Python,
|
||||
python: PythonProcess,
|
||||
settings_module: String,
|
||||
installed_apps: Apps,
|
||||
templatetags: TemplateTags,
|
||||
) -> Self {
|
||||
Self {
|
||||
py,
|
||||
python,
|
||||
settings_module,
|
||||
installed_apps,
|
||||
templatetags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<Self, ProjectError> {
|
||||
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
|
||||
let settings_module =
|
||||
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
|
||||
|
||||
let py = Python::initialize()?;
|
||||
let py = Python::setup(&mut python)?;
|
||||
|
||||
let has_django = ImportCheck::check(&py, "django")?;
|
||||
let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?;
|
||||
|
||||
if !has_django {
|
||||
return Err(ProjectError::DjangoNotFound);
|
||||
}
|
||||
|
||||
if !check_gis_setup(&py)? {
|
||||
if !check_gis_setup(&mut python)? {
|
||||
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,
|
||||
settings_module,
|
||||
installed_apps: Apps::default(),
|
||||
templatetags: TemplateTags::default(),
|
||||
});
|
||||
}
|
||||
|
||||
let setup = DjangoSetup::run_with_py(&py)?;
|
||||
let response = DjangoSetup::setup(&mut python)?;
|
||||
let setup: DjangoSetup = response
|
||||
.data()
|
||||
.clone()
|
||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?;
|
||||
|
||||
Ok(Self::new(
|
||||
py,
|
||||
python,
|
||||
settings_module,
|
||||
Apps::from_strings(setup.installed_apps.to_vec()),
|
||||
setup.templatetags,
|
||||
|
@ -110,8 +124,11 @@ pub enum ProjectError {
|
|||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] djls_python::PythonError),
|
||||
Packaging(#[from] djls_python::PackagingError),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] RunnerError),
|
||||
Python(#[from] djls_python::PythonError),
|
||||
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::apps::Apps;
|
||||
use djls_python::{Python, RunnerError};
|
||||
use djls_ipc::{PythonProcess, TransportError};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn check_gis_setup(py: &Python) -> Result<bool, GISError> {
|
||||
let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?;
|
||||
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
|
||||
let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?;
|
||||
let gdal_is_installed = Command::new("gdalinfo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
|
@ -21,6 +21,6 @@ pub enum GISError {
|
|||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] RunnerError),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
mod apps;
|
||||
mod django;
|
||||
mod gis;
|
||||
mod scripts;
|
||||
mod templates;
|
||||
|
||||
pub use django::DjangoProject;
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
use djls_python::include_script;
|
||||
|
||||
pub const DJANGO_SETUP: &str = include_script!("django_setup");
|
||||
pub const INSTALLED_APPS_CHECK: &str = include_script!("installed_apps_check");
|
|
@ -8,6 +8,7 @@ anyhow = { workspace = true }
|
|||
async-trait = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
tempfile = "3.14.0"
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
mod client;
|
||||
mod process;
|
||||
mod server;
|
||||
mod transport;
|
||||
|
||||
pub use client::Client;
|
||||
pub use process::PythonProcess;
|
||||
pub use server::Server;
|
||||
pub use transport::parse_json_response;
|
||||
pub use transport::parse_raw_response;
|
||||
pub use transport::JsonResponse;
|
||||
pub use transport::Transport;
|
||||
pub use transport::TransportError;
|
||||
|
|
81
crates/djls-ipc/src/process.rs
Normal file
81
crates/djls-ipc/src/process.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use crate::transport::{Transport, TransportError, TransportProtocol};
|
||||
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<Box<dyn TransportProtocol>>>,
|
||||
_child: Child,
|
||||
healthy: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PythonProcess {
|
||||
pub fn new(
|
||||
module: &str,
|
||||
transport: Transport,
|
||||
health_check_interval: Option<Duration>,
|
||||
) -> Result<Self, TransportError> {
|
||||
let mut child = Command::new("python")
|
||||
.arg("-m")
|
||||
.arg(module)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().unwrap();
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
|
||||
let process = Self {
|
||||
transport: Arc::new(Mutex::new(transport.create(stdin, stdout)?)),
|
||||
_child: child,
|
||||
healthy: Arc::new(AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
if let Some(interval) = health_check_interval {
|
||||
process.start_health_check_task(interval)?;
|
||||
}
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
fn start_health_check_task(&self, interval: Duration) -> Result<(), TransportError> {
|
||||
let healthy = self.healthy.clone();
|
||||
let transport = self.transport.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if let Ok(mut transport) = transport.lock() {
|
||||
match transport.health_check() {
|
||||
Ok(()) => {
|
||||
healthy.store(true, Ordering::SeqCst);
|
||||
}
|
||||
Err(_) => {
|
||||
healthy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_healthy(&self) -> bool {
|
||||
self.healthy.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn send(
|
||||
&mut self,
|
||||
message: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<String, TransportError> {
|
||||
let mut transport = self.transport.lock().unwrap();
|
||||
transport.send(message, args)
|
||||
}
|
||||
}
|
214
crates/djls-ipc/src/transport.rs
Normal file
214
crates/djls-ipc/src/transport.rs
Normal file
|
@ -0,0 +1,214 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fmt::Debug;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
use std::process::{ChildStdin, ChildStdout};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TransportError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Process error: {0}")]
|
||||
Process(String),
|
||||
}
|
||||
|
||||
pub enum Transport {
|
||||
Raw,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn create(
|
||||
&self,
|
||||
mut stdin: ChildStdin,
|
||||
mut stdout: ChildStdout,
|
||||
) -> Result<Box<dyn TransportProtocol>, TransportError> {
|
||||
let transport_type = match self {
|
||||
Transport::Raw => "raw",
|
||||
Transport::Json => "json",
|
||||
};
|
||||
|
||||
writeln!(stdin, "{}", transport_type).map_err(TransportError::Io)?;
|
||||
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(TransportError::Process(
|
||||
"Python process not ready".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
match self {
|
||||
Transport::Raw => Ok(Box::new(RawTransport::new(stdin, stdout)?)),
|
||||
Transport::Json => Ok(Box::new(JsonTransport::new(stdin, stdout)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TransportProtocol: Debug + Send {
|
||||
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn health_check(&mut self) -> Result<(), TransportError>;
|
||||
fn clone_box(&self) -> Box<dyn TransportProtocol>;
|
||||
fn send_impl(
|
||||
&mut self,
|
||||
message: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<String, TransportError>;
|
||||
|
||||
fn send(&mut self, message: &str, args: Option<Vec<String>>) -> Result<String, TransportError> {
|
||||
self.health_check()?;
|
||||
self.send_impl(message, args)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn TransportProtocol> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawTransport {
|
||||
reader: Arc<Mutex<BufReader<ChildStdout>>>,
|
||||
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
}
|
||||
|
||||
impl TransportProtocol for RawTransport {
|
||||
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
|
||||
Ok(Self {
|
||||
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
|
||||
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
|
||||
})
|
||||
}
|
||||
|
||||
fn health_check(&mut self) -> Result<(), TransportError> {
|
||||
self.send_impl("health", None)
|
||||
.and_then(|response| match response.as_str() {
|
||||
"ok" => Ok(()),
|
||||
other => Err(TransportError::Process(format!(
|
||||
"Health check failed: {}",
|
||||
other
|
||||
))),
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn TransportProtocol> {
|
||||
Box::new(RawTransport {
|
||||
reader: self.reader.clone(),
|
||||
writer: self.writer.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_impl(
|
||||
&mut self,
|
||||
message: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<String, TransportError> {
|
||||
let mut writer = self.writer.lock().unwrap();
|
||||
|
||||
if let Some(args) = args {
|
||||
// Join command and args with spaces
|
||||
writeln!(writer, "{} {}", message, args.join(" ")).map_err(TransportError::Io)?;
|
||||
} else {
|
||||
writeln!(writer, "{}", message).map_err(TransportError::Io)?;
|
||||
}
|
||||
|
||||
writer.flush().map_err(TransportError::Io)?;
|
||||
|
||||
let mut reader = self.reader.lock().unwrap();
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).map_err(TransportError::Io)?;
|
||||
Ok(line.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct JsonCommand {
|
||||
command: String,
|
||||
args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JsonResponse {
|
||||
status: String,
|
||||
data: Option<Value>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl JsonResponse {
|
||||
pub fn data(&self) -> &Option<Value> {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JsonTransport {
|
||||
reader: Arc<Mutex<BufReader<ChildStdout>>>,
|
||||
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
|
||||
}
|
||||
|
||||
impl TransportProtocol for JsonTransport {
|
||||
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
|
||||
Ok(Self {
|
||||
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
|
||||
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
|
||||
})
|
||||
}
|
||||
|
||||
fn health_check(&mut self) -> Result<(), TransportError> {
|
||||
self.send_impl("health", None).and_then(|response| {
|
||||
let json: JsonResponse = serde_json::from_str(&response)?;
|
||||
match json.status.as_str() {
|
||||
"ok" => Ok(()),
|
||||
_ => Err(TransportError::Process(
|
||||
json.error.unwrap_or_else(|| "Unknown error".to_string()),
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn TransportProtocol> {
|
||||
Box::new(JsonTransport {
|
||||
reader: self.reader.clone(),
|
||||
writer: self.writer.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_impl(
|
||||
&mut self,
|
||||
message: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> Result<String, TransportError> {
|
||||
let command = JsonCommand {
|
||||
command: message.to_string(),
|
||||
args,
|
||||
};
|
||||
|
||||
let mut writer = self.writer.lock().unwrap();
|
||||
serde_json::to_writer(&mut *writer, &command)?;
|
||||
writeln!(writer).map_err(TransportError::Io)?;
|
||||
writer.flush().map_err(TransportError::Io)?;
|
||||
|
||||
let mut reader = self.reader.lock().unwrap();
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).map_err(TransportError::Io)?;
|
||||
Ok(line.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_raw_response(response: String) -> Result<String, TransportError> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn parse_json_response(response: String) -> Result<JsonResponse, TransportError> {
|
||||
serde_json::from_str(&response).map_err(TransportError::Json)
|
||||
}
|
|
@ -4,6 +4,8 @@ version = "0.0.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
djls-ipc = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
mod packaging;
|
||||
mod python;
|
||||
mod runner;
|
||||
mod scripts;
|
||||
|
||||
pub use crate::packaging::ImportCheck;
|
||||
pub use crate::packaging::PackagingError;
|
||||
pub use crate::python::Python;
|
||||
pub use crate::python::PythonError;
|
||||
pub use crate::runner::Runner;
|
||||
pub use crate::runner::RunnerError;
|
||||
pub use crate::runner::ScriptRunner;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use crate::python::Python;
|
||||
use crate::runner::{RunnerError, ScriptRunner};
|
||||
use crate::scripts;
|
||||
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
@ -59,8 +57,16 @@ pub struct ImportCheck {
|
|||
can_import: bool,
|
||||
}
|
||||
|
||||
impl ScriptRunner for ImportCheck {
|
||||
const SCRIPT: &'static str = scripts::HAS_IMPORT;
|
||||
impl TryFrom<JsonResponse> for ImportCheck {
|
||||
type Error = TransportError;
|
||||
|
||||
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
|
||||
response
|
||||
.data()
|
||||
.clone()
|
||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportCheck {
|
||||
|
@ -68,9 +74,14 @@ impl ImportCheck {
|
|||
self.can_import
|
||||
}
|
||||
|
||||
pub fn check(py: &Python, module: &str) -> Result<bool, RunnerError> {
|
||||
let result = ImportCheck::run_with_py_args(py, module)?;
|
||||
Ok(result.can_import)
|
||||
pub fn check(
|
||||
python: &mut PythonProcess,
|
||||
modules: Option<Vec<String>>,
|
||||
) -> Result<bool, PackagingError> {
|
||||
let response = python.send("has_import", modules)?;
|
||||
let response = parse_json_response(response)?;
|
||||
let check = Self::try_from(response)?;
|
||||
Ok(check.can_import)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,15 +93,9 @@ pub enum PackagingError {
|
|||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] Box<RunnerError>),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl From<RunnerError> for PackagingError {
|
||||
fn from(err: RunnerError) -> Self {
|
||||
PackagingError::Runner(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use crate::packaging::{Packages, PackagingError};
|
||||
use crate::runner::{Runner, RunnerError, ScriptRunner};
|
||||
use crate::scripts;
|
||||
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use which::which;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct VersionInfo {
|
||||
|
@ -60,29 +58,23 @@ pub struct Python {
|
|||
packages: Packages,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PythonSetup(Python);
|
||||
impl TryFrom<JsonResponse> for Python {
|
||||
type Error = TransportError;
|
||||
|
||||
impl ScriptRunner for PythonSetup {
|
||||
const SCRIPT: &'static str = scripts::PYTHON_SETUP;
|
||||
}
|
||||
|
||||
impl From<PythonSetup> for Python {
|
||||
fn from(setup: PythonSetup) -> Self {
|
||||
setup.0
|
||||
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
|
||||
response
|
||||
.data()
|
||||
.clone()
|
||||
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
||||
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
|
||||
}
|
||||
}
|
||||
|
||||
impl Python {
|
||||
pub fn initialize() -> Result<Self, PythonError> {
|
||||
let executable = which("python")?;
|
||||
Ok(PythonSetup::run_with_path(&executable)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Runner for Python {
|
||||
fn get_executable(&self) -> &Path {
|
||||
&self.sys_executable
|
||||
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
|
||||
let response = python.send("python_setup", None)?;
|
||||
let response = parse_json_response(response)?;
|
||||
Ok(Self::try_from(response)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,15 +115,9 @@ pub enum PythonError {
|
|||
#[error("Failed to locate Python executable: {0}")]
|
||||
PythonNotFound(#[from] which::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Runner(#[from] Box<RunnerError>),
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl From<RunnerError> for PythonError {
|
||||
fn from(err: RunnerError) -> Self {
|
||||
PythonError::Runner(Box::new(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
use crate::python::{Python, PythonError};
|
||||
use serde::ser::Error;
|
||||
use serde::Deserialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub trait Runner {
|
||||
fn get_executable(&self) -> &Path;
|
||||
|
||||
fn run_module(&self, command: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.arg("-m")
|
||||
.arg(command)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Command failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_module_with_args(&self, command: &str, args: &[&str]) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.arg("-m")
|
||||
.arg(command)
|
||||
.args(args)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Command failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_python_code(&self, code: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.args(["-c", code])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Python execution failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_python_code_with_args(&self, code: &str, args: &str) -> std::io::Result<String> {
|
||||
let output = Command::new(self.get_executable())
|
||||
.args(["-c", code, args])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Python execution failed: {}", error),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn run_script<T>(&self, script: &str) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = self
|
||||
.run_python_code(script)
|
||||
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
serde_json::from_str(&result)
|
||||
}
|
||||
|
||||
fn run_script_with_args<T>(&self, script: &str, args: &str) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = self
|
||||
.run_python_code_with_args(script, args)
|
||||
.map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
serde_json::from_str(&result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SimpleRunner {
|
||||
executable: PathBuf,
|
||||
}
|
||||
|
||||
impl SimpleRunner {
|
||||
pub fn new(executable: PathBuf) -> Self {
|
||||
Self { executable }
|
||||
}
|
||||
}
|
||||
|
||||
impl Runner for SimpleRunner {
|
||||
fn get_executable(&self) -> &Path {
|
||||
&self.executable
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ScriptRunner: Sized {
|
||||
const SCRIPT: &'static str;
|
||||
|
||||
fn run_with_exe<R: Runner>(runner: &R) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = runner.run_script(Self::SCRIPT).map_err(RunnerError::from)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_with_exe_args<R: Runner>(runner: &R, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let result = runner
|
||||
.run_script_with_args(Self::SCRIPT, args)
|
||||
.map_err(RunnerError::from)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_with_path(executable: &Path) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let runner = &SimpleRunner::new(executable.to_path_buf());
|
||||
Self::run_with_exe(runner)
|
||||
}
|
||||
|
||||
fn run_with_path_args(executable: &Path, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let runner = &SimpleRunner::new(executable.to_path_buf());
|
||||
Self::run_with_exe_args(runner, args)
|
||||
}
|
||||
|
||||
fn run_with_py(python: &Python) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Self::run_with_exe(python)
|
||||
}
|
||||
|
||||
fn run_with_py_args(python: &Python, args: &str) -> Result<Self, RunnerError>
|
||||
where
|
||||
Self: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Self::run_with_exe_args(python, args)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunnerError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Python(#[from] PythonError),
|
||||
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_export]
|
||||
macro_rules! include_script {
|
||||
($name:expr) => {
|
||||
include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"python/djls/scripts/",
|
||||
$name,
|
||||
".py"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
pub const HAS_IMPORT: &str = include_script!("has_import");
|
||||
pub const PYTHON_SETUP: &str = include_script!["python_setup"];
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
djls-ast = { workspace = true }
|
||||
djls-django = { workspace = true }
|
||||
djls-ipc = { workspace = true }
|
||||
djls-python = { workspace = true }
|
||||
djls-worker = { workspace = true }
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ 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;
|
||||
|
@ -80,8 +81,8 @@ impl LanguageServer for TowerLspBackend {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn serve() -> Result<()> {
|
||||
let django = DjangoProject::setup()?;
|
||||
pub async fn serve(python: PythonProcess) -> Result<()> {
|
||||
let django = DjangoProject::setup(python)?;
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
|
|
@ -16,7 +16,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["python/djls"]
|
||||
sources = ["python"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
|
104
python/djls/lsp.py
Normal file
104
python/djls/lsp.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from .scripts import django_setup
|
||||
from .scripts import has_import
|
||||
from .scripts import python_setup
|
||||
|
||||
|
||||
def handle_command(command: str) -> str:
|
||||
parts = command.strip().split()
|
||||
command = parts[0]
|
||||
args = parts[1:] if len(parts) > 1 else []
|
||||
|
||||
if command == "django_setup":
|
||||
return json.dumps(django_setup.get_django_setup_info())
|
||||
if command == "has_import":
|
||||
if not args:
|
||||
return "error: Missing module name argument"
|
||||
return json.dumps({"can_import": has_import.check_import(args[0])})
|
||||
if command == "health":
|
||||
return "ok"
|
||||
if command == "installed_apps_check":
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
django.setup()
|
||||
if not args:
|
||||
return "error: Missing module name argument"
|
||||
return json.dumps({"has_app": args[0] in settings.INSTALLED_APPS})
|
||||
if command == "python_setup":
|
||||
return json.dumps(python_setup.get_python_info())
|
||||
if command == "version":
|
||||
return "0.1.0"
|
||||
return f"Unknown command: {command}"
|
||||
|
||||
|
||||
def handle_json_command(data: dict[str, Any]) -> dict[str, Any]:
|
||||
command = data["command"]
|
||||
args = data.get("args", []) # Get args if they exist
|
||||
|
||||
if command == "django_setup":
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
return {"status": "ok", "data": django_setup.get_django_setup_info()}
|
||||
if command == "has_import":
|
||||
if not args:
|
||||
return {"status": "error", "error": "Missing module name argument"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {"can_import": has_import.check_import(args[0])},
|
||||
}
|
||||
if command == "health":
|
||||
return {"status": "ok"}
|
||||
if command == "installed_apps_check":
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
django.setup()
|
||||
if not args:
|
||||
return {"status": "error", "error": "Missing module name argument"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {"has_app": args[0] in settings.INSTALLED_APPS},
|
||||
}
|
||||
if command == "python_setup":
|
||||
return {"status": "ok", "data": python_setup.get_python_info()}
|
||||
if command == "version":
|
||||
return {"status": "ok", "data": "0.1.0"}
|
||||
|
||||
return {"status": "error", "error": f"Unknown command: {command}"}
|
||||
|
||||
|
||||
def main():
|
||||
transport_type = sys.stdin.readline().strip()
|
||||
print("ready", flush=True)
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
if transport_type == "json":
|
||||
data = json.loads(line)
|
||||
response = handle_json_command(data)
|
||||
print(json.dumps(response), flush=True)
|
||||
else:
|
||||
command = line.strip()
|
||||
response = handle_command(command)
|
||||
print(response, flush=True)
|
||||
|
||||
except Exception as e:
|
||||
if transport_type == "json":
|
||||
print(json.dumps({"status": "error", "error": str(e)}), flush=True)
|
||||
else:
|
||||
print(f"error: {str(e)}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -2,11 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.engine import Engine
|
||||
|
||||
|
||||
def get_django_setup_info():
|
||||
from django.conf import settings
|
||||
from django.template.engine import Engine
|
||||
|
||||
return {
|
||||
"installed_apps": list(settings.INSTALLED_APPS),
|
||||
"templatetags": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue