mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-23 04:15:04 +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]
|
[dependencies]
|
||||||
djls-django = { workspace = true }
|
djls-django = { workspace = true }
|
||||||
|
djls-ipc = { workspace = true }
|
||||||
djls-server = { workspace = true }
|
djls-server = { workspace = true }
|
||||||
|
|
||||||
anyhow = { 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)]
|
#[derive(Debug, Parser)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
@ -6,10 +8,35 @@ struct Cli {
|
||||||
command: Commands,
|
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)]
|
#[derive(Debug, Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Start the LSP server
|
/// Start the LSP server
|
||||||
Serve,
|
Serve(CommonOpts),
|
||||||
|
/// Get Python environment information
|
||||||
|
Info(CommonOpts),
|
||||||
|
/// Print the version
|
||||||
|
Version(CommonOpts),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -17,7 +44,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
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(())
|
Ok(())
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
djls-ipc = { workspace = true }
|
||||||
djls-python = { workspace = true }
|
djls-python = { workspace = true }
|
||||||
|
|
||||||
serde = { 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 serde::Deserialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::scripts;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct App(String);
|
pub struct App(String);
|
||||||
|
|
||||||
|
@ -27,8 +25,16 @@ struct InstalledAppsCheck {
|
||||||
has_app: bool,
|
has_app: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptRunner for InstalledAppsCheck {
|
impl TryFrom<JsonResponse> for InstalledAppsCheck {
|
||||||
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK;
|
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 {
|
impl Apps {
|
||||||
|
@ -48,8 +54,10 @@ impl Apps {
|
||||||
self.apps().iter()
|
self.apps().iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> {
|
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
|
||||||
let result = InstalledAppsCheck::run_with_py_args(py, app)?;
|
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)
|
Ok(result.has_app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::apps::Apps;
|
use crate::apps::Apps;
|
||||||
use crate::gis::{check_gis_setup, GISError};
|
use crate::gis::{check_gis_setup, GISError};
|
||||||
use crate::scripts;
|
|
||||||
use crate::templates::TemplateTags;
|
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 serde::Deserialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DjangoProject {
|
pub struct DjangoProject {
|
||||||
py: Python,
|
py: Python,
|
||||||
|
python: PythonProcess,
|
||||||
settings_module: String,
|
settings_module: String,
|
||||||
installed_apps: Apps,
|
installed_apps: Apps,
|
||||||
templatetags: TemplateTags,
|
templatetags: TemplateTags,
|
||||||
|
@ -20,54 +21,67 @@ struct DjangoSetup {
|
||||||
templatetags: TemplateTags,
|
templatetags: TemplateTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptRunner for DjangoSetup {
|
impl DjangoSetup {
|
||||||
const SCRIPT: &'static str = scripts::DJANGO_SETUP;
|
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 {
|
impl DjangoProject {
|
||||||
fn new(
|
fn new(
|
||||||
py: Python,
|
py: Python,
|
||||||
|
python: PythonProcess,
|
||||||
settings_module: String,
|
settings_module: String,
|
||||||
installed_apps: Apps,
|
installed_apps: Apps,
|
||||||
templatetags: TemplateTags,
|
templatetags: TemplateTags,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
py,
|
py,
|
||||||
|
python,
|
||||||
settings_module,
|
settings_module,
|
||||||
installed_apps,
|
installed_apps,
|
||||||
templatetags,
|
templatetags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup() -> Result<Self, ProjectError> {
|
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
|
||||||
let settings_module =
|
let settings_module =
|
||||||
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
|
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 {
|
if !has_django {
|
||||||
return Err(ProjectError::DjangoNotFound);
|
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!("Warning: GeoDjango detected but GDAL is not available.");
|
||||||
eprintln!("Django initialization will be skipped. Some features may be limited.");
|
eprintln!("Django initialization will be skipped. Some features may be limited.");
|
||||||
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
|
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
|
||||||
|
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
py,
|
py,
|
||||||
|
python,
|
||||||
settings_module,
|
settings_module,
|
||||||
installed_apps: Apps::default(),
|
installed_apps: Apps::default(),
|
||||||
templatetags: TemplateTags::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(
|
Ok(Self::new(
|
||||||
py,
|
py,
|
||||||
|
python,
|
||||||
settings_module,
|
settings_module,
|
||||||
Apps::from_strings(setup.installed_apps.to_vec()),
|
Apps::from_strings(setup.installed_apps.to_vec()),
|
||||||
setup.templatetags,
|
setup.templatetags,
|
||||||
|
@ -110,8 +124,11 @@ pub enum ProjectError {
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Python(#[from] djls_python::PythonError),
|
Packaging(#[from] djls_python::PackagingError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[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 crate::apps::Apps;
|
||||||
use djls_python::{Python, RunnerError};
|
use djls_ipc::{PythonProcess, TransportError};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn check_gis_setup(py: &Python) -> Result<bool, GISError> {
|
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
|
||||||
let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?;
|
let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?;
|
||||||
let gdal_is_installed = Command::new("gdalinfo")
|
let gdal_is_installed = Command::new("gdalinfo")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
|
@ -21,6 +21,6 @@ pub enum GISError {
|
||||||
#[error("JSON parsing error: {0}")]
|
#[error("JSON parsing error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error("Transport error: {0}")]
|
||||||
Runner(#[from] RunnerError),
|
Transport(#[from] TransportError),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
mod apps;
|
mod apps;
|
||||||
mod django;
|
mod django;
|
||||||
mod gis;
|
mod gis;
|
||||||
mod scripts;
|
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
pub use django::DjangoProject;
|
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 }
|
async-trait = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
tempfile = "3.14.0"
|
tempfile = "3.14.0"
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
mod client;
|
mod client;
|
||||||
|
mod process;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod transport;
|
||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
|
pub use process::PythonProcess;
|
||||||
pub use server::Server;
|
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"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
djls-ipc = { workspace = true }
|
||||||
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
mod packaging;
|
mod packaging;
|
||||||
mod python;
|
mod python;
|
||||||
mod runner;
|
|
||||||
mod scripts;
|
|
||||||
|
|
||||||
pub use crate::packaging::ImportCheck;
|
pub use crate::packaging::ImportCheck;
|
||||||
|
pub use crate::packaging::PackagingError;
|
||||||
pub use crate::python::Python;
|
pub use crate::python::Python;
|
||||||
pub use crate::python::PythonError;
|
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 djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||||
use crate::runner::{RunnerError, ScriptRunner};
|
|
||||||
use crate::scripts;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -59,8 +57,16 @@ pub struct ImportCheck {
|
||||||
can_import: bool,
|
can_import: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptRunner for ImportCheck {
|
impl TryFrom<JsonResponse> for ImportCheck {
|
||||||
const SCRIPT: &'static str = scripts::HAS_IMPORT;
|
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 {
|
impl ImportCheck {
|
||||||
|
@ -68,9 +74,14 @@ impl ImportCheck {
|
||||||
self.can_import
|
self.can_import
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(py: &Python, module: &str) -> Result<bool, RunnerError> {
|
pub fn check(
|
||||||
let result = ImportCheck::run_with_py_args(py, module)?;
|
python: &mut PythonProcess,
|
||||||
Ok(result.can_import)
|
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}")]
|
#[error("JSON parsing error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error("Transport error: {0}")]
|
||||||
Runner(#[from] Box<RunnerError>),
|
Transport(#[from] TransportError),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("UTF-8 conversion error: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
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::packaging::{Packages, PackagingError};
|
||||||
use crate::runner::{Runner, RunnerError, ScriptRunner};
|
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
|
||||||
use crate::scripts;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use which::which;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct VersionInfo {
|
pub struct VersionInfo {
|
||||||
|
@ -60,29 +58,23 @@ pub struct Python {
|
||||||
packages: Packages,
|
packages: Packages,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
impl TryFrom<JsonResponse> for Python {
|
||||||
pub struct PythonSetup(Python);
|
type Error = TransportError;
|
||||||
|
|
||||||
impl ScriptRunner for PythonSetup {
|
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
|
||||||
const SCRIPT: &'static str = scripts::PYTHON_SETUP;
|
response
|
||||||
}
|
.data()
|
||||||
|
.clone()
|
||||||
impl From<PythonSetup> for Python {
|
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
|
||||||
fn from(setup: PythonSetup) -> Self {
|
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
|
||||||
setup.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Python {
|
impl Python {
|
||||||
pub fn initialize() -> Result<Self, PythonError> {
|
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
|
||||||
let executable = which("python")?;
|
let response = python.send("python_setup", None)?;
|
||||||
Ok(PythonSetup::run_with_path(&executable)?.into())
|
let response = parse_json_response(response)?;
|
||||||
}
|
Ok(Self::try_from(response)?)
|
||||||
}
|
|
||||||
|
|
||||||
impl Runner for Python {
|
|
||||||
fn get_executable(&self) -> &Path {
|
|
||||||
&self.sys_executable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,15 +115,9 @@ pub enum PythonError {
|
||||||
#[error("Failed to locate Python executable: {0}")]
|
#[error("Failed to locate Python executable: {0}")]
|
||||||
PythonNotFound(#[from] which::Error),
|
PythonNotFound(#[from] which::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error("Transport error: {0}")]
|
||||||
Runner(#[from] Box<RunnerError>),
|
Transport(#[from] TransportError),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("UTF-8 conversion error: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
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]
|
[dependencies]
|
||||||
djls-ast = { workspace = true }
|
djls-ast = { workspace = true }
|
||||||
djls-django = { workspace = true }
|
djls-django = { workspace = true }
|
||||||
|
djls-ipc = { workspace = true }
|
||||||
djls-python = { workspace = true }
|
djls-python = { workspace = true }
|
||||||
djls-worker = { workspace = true }
|
djls-worker = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::notifier::TowerLspNotifier;
|
||||||
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use djls_django::DjangoProject;
|
use djls_django::DjangoProject;
|
||||||
|
use djls_ipc::PythonProcess;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower_lsp::jsonrpc::Result as LspResult;
|
use tower_lsp::jsonrpc::Result as LspResult;
|
||||||
|
@ -80,8 +81,8 @@ impl LanguageServer for TowerLspBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve() -> Result<()> {
|
pub async fn serve(python: PythonProcess) -> Result<()> {
|
||||||
let django = DjangoProject::setup()?;
|
let django = DjangoProject::setup(python)?;
|
||||||
|
|
||||||
let stdin = tokio::io::stdin();
|
let stdin = tokio::io::stdin();
|
||||||
let stdout = tokio::io::stdout();
|
let stdout = tokio::io::stdout();
|
||||||
|
|
|
@ -16,7 +16,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
include = ["python/djls"]
|
sources = ["python"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
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
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def get_django_setup_info():
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
|
|
||||||
|
|
||||||
def get_django_setup_info():
|
|
||||||
return {
|
return {
|
||||||
"installed_apps": list(settings.INSTALLED_APPS),
|
"installed_apps": list(settings.INSTALLED_APPS),
|
||||||
"templatetags": [
|
"templatetags": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue